Skip to content
Sapphire UIv4.1.0

Admin Layout

Frame an application with a sidebar and a main workspace. Use it for admin tools with persistent navigation across several sections.

Examples

Workspace

JSX usage

Install this example’s dependencies, load the package stylesheet from Setup, and add Example CSS where shown. The snippet includes its local helpers.

bun add @kongmy-dev/sapphire-ui react react-dom lucide-react radix-ui
"use client";
import { Plus } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Badge } from "@kongmy-dev/sapphire-ui/badge";
import { Button } from "@kongmy-dev/sapphire-ui/button";
import { Card } from "@kongmy-dev/sapphire-ui/card";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@kongmy-dev/sapphire-ui/dialog";
import { Empty } from "@kongmy-dev/sapphire-ui/empty";
import { FormField } from "@kongmy-dev/sapphire-ui/form-field";
import { Input } from "@kongmy-dev/sapphire-ui/input";
import { NativeSelect } from "@kongmy-dev/sapphire-ui/native-select";
import { PageHeader } from "@kongmy-dev/sapphire-ui/page-header";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kongmy-dev/sapphire-ui/table";
import { Archive, ChevronDown, FolderKanban, LayoutDashboard, Settings } from "lucide-react";
import { useId } from "react";
import { Breadcrumb } from "@kongmy-dev/sapphire-ui/breadcrumb";
import { Checkbox } from "@kongmy-dev/sapphire-ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@kongmy-dev/sapphire-ui/dropdown-menu";
import { FormSection } from "@kongmy-dev/sapphire-ui/form-section";
import { Stat } from "@kongmy-dev/sapphire-ui/stat";
import { StatGroup } from "@kongmy-dev/sapphire-ui/stat-group";
import { ThemeToggle } from "@kongmy-dev/sapphire-ui/theme-toggle";
import { AdminHeader, AdminLayout, AdminSidebar, AdminSidebarGroup, AdminSidebarLink, AdminSidebarSection } from "@kongmy-dev/sapphire-ui/admin-layout";

export const initialProjects = [
  {
    name: 'Customer portal',
    client: 'Northstar',
    status: 'Active',
    updated: 'Today',
    owner: 'AR',
  },
  {
    name: 'Inventory refresh',
    client: 'Fieldwork',
    status: 'Active',
    updated: 'Today',
    owner: 'JL',
  },
  {
    name: 'Operations dashboard',
    client: 'Meridian',
    status: 'Review',
    updated: 'Yesterday',
    owner: 'AR',
  },
  {
    name: 'Brand website',
    client: 'Northstar',
    status: 'Active',
    updated: '22 Oct',
    owner: 'SK',
  },
  {
    name: 'Billing integration',
    client: 'Fieldwork',
    status: 'Review',
    updated: '21 Oct',
    owner: 'JL',
  },
  {
    name: 'Member directory',
    client: 'Meridian',
    status: 'Archived',
    updated: '18 Oct',
    owner: 'SK',
  },
] as const;
export type Project = {
  name: string;
  client: string;
  status: string;
  updated: string;
  owner: string;
};
export function NewProject({
  onCreate,
}: {
  onCreate: (project: Project) => void;
}) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  function changeOpen(next: boolean) {
    setOpen(next);
    if (!next) {
      setName('');
      setError('');
    }
  }
  return (
    <Dialog open={open} onOpenChange={changeOpen}>
      <DialogTrigger asChild>
        <Button icon={<Plus className="sp-icon" aria-hidden="true" />}>
          New project
        </Button>
      </DialogTrigger>
      <DialogContent>
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            if (!name.trim()) {
              setError('Enter a project name.');
              return;
            }
            onCreate({
              name: name.trim(),
              client: 'Your workspace',
              status: 'Active',
              updated: 'Just now',
              owner: 'AR',
            });
            changeOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Create project</DialogTitle>
            <DialogDescription>
              Start a project for your team.
            </DialogDescription>
          </DialogHeader>
          <DialogBody>
            <FormField label="Project name" required error={error}>
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setError('');
                }}
              />
            </FormField>
          </DialogBody>
          <DialogFooter>
            <Button
              type="button"
              variant="outline"
              onClick={() => changeOpen(false)}
            >
              Cancel
            </Button>
            <Button type="submit">Create project</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}
export function ProjectTable({ projects }: { projects: readonly Project[] }) {
  return (
    <Table aria-label="Projects">
      <TableHeader>
        <TableRow>
          <TableHead scope="col">Project</TableHead>
          <TableHead scope="col">Status</TableHead>
          <TableHead scope="col">Owner</TableHead>
          <TableHead scope="col">Updated</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {projects.map((project) => (
          <TableRow key={project.name}>
            <TableCell>
              <strong>{project.name}</strong>
              <span className="demo-project-client">{project.client}</span>
            </TableCell>
            <TableCell>
              <Badge
                variant={project.status === 'Active' ? 'success' : 'default'}
              >
                {project.status}
              </Badge>
            </TableCell>
            <TableCell>
              <span
                className="demo-workspace-avatar"
                role="img"
                aria-label={`Owner ${project.owner}`}
              >
                {project.owner}
              </span>
            </TableCell>
            <TableCell className="demo-project-updated">
              {project.updated}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
export function WorkspaceProjects({
  projects,
  actions,
  archived,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  archived: boolean;
}) {
  const [search, setSearch] = useState('');
  const [status, setStatus] = useState(archived ? 'Archived' : 'All');
  const [page, setPage] = useState(0);
  const filtered = projects.filter(
    (project) =>
      `${project.name} ${project.client}`
        .toLowerCase()
        .includes(search.toLowerCase()) &&
      (status === 'All' || project.status === status),
  );
  const pageCount = Math.max(1, Math.ceil(filtered.length / 4));
  const currentPage = Math.min(page, pageCount - 1);
  return (
    <>
      <PageHeader
        title={archived ? 'Archived projects' : 'Projects'}
        description="Find a project and see what needs attention."
        actions={actions}
      />
      <Card padding="none">
        <div className="demo-workspace-filters">
          <Input
            type="search"
            aria-label="Search projects"
            placeholder="Search projects or clients…"
            value={search}
            onChange={(event) => {
              setSearch(event.target.value);
              setPage(0);
            }}
          />
          <NativeSelect
            aria-label="Project status"
            value={status}
            onChange={(event) => {
              setStatus(event.target.value);
              setPage(0);
            }}
          >
            <option value="All">All statuses</option>
            <option>Active</option>
            <option>Review</option>
            <option>Archived</option>
          </NativeSelect>
        </div>
        {filtered.length ? (
          <ProjectTable
            projects={filtered.slice(currentPage * 4, currentPage * 4 + 4)}
          />
        ) : (
          <Empty
            title="No projects found"
            description="Try another search or clear your filters."
            action={
              <Button
                variant="outline"
                onClick={() => {
                  setSearch('');
                  setStatus('All');
                }}
              >
                Clear filters
              </Button>
            }
          />
        )}
        <ProjectPagination
          currentPage={currentPage}
          pageCount={pageCount}
          total={filtered.length}
          onPageChange={setPage}
        />
      </Card>
    </>
  );
}
function ProjectPagination({
  currentPage,
  pageCount,
  total,
  onPageChange,
}: {
  currentPage: number;
  pageCount: number;
  total: number;
  onPageChange: (page: number) => void;
}) {
  return (
    <div className="demo-workspace-pagination">
      <span role="status">
        {total} projects · Page {currentPage + 1} of {pageCount}
      </span>
      <div className="sp-cluster">
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage === 0}
          onClick={() => onPageChange(currentPage - 1)}
        >
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage + 1 === pageCount}
          onClick={() => onPageChange(currentPage + 1)}
        >
          Next
        </Button>
      </div>
    </div>
  );
}
type Page = 'overview' | 'projects' | 'archived' | 'settings';
const titles = {
  overview: 'Overview',
  projects: 'Projects',
  archived: 'Archived projects',
  settings: 'Workspace settings',
} as const;
function WorkspaceBrand({ compact }: { compact: boolean }) {
  return (
    <a
      className="demo-workspace-identity"
      href="#workspace"
      aria-label={compact ? 'Sapphire workspace' : 'S Sapphire Team workspace'}
    >
      <span className="demo-workspace-mark" aria-hidden="true">
        S
      </span>{' '}
      {!compact && (
        <span>
          <strong>Sapphire</strong> <small>Team workspace</small>
        </span>
      )}
    </a>
  );
}
function WorkspaceAccount({
  compact,
  onSettings,
}: {
  compact: boolean;
  onSettings: () => void;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          className="demo-workspace-identity demo-workspace-account"
          aria-label={
            compact
              ? 'AR Aisha Rahman account'
              : 'AR Aisha Rahman Workspace admin account'
          }
        >
          <span className="demo-workspace-avatar" aria-hidden="true">
            AR
          </span>{' '}
          {!compact && (
            <>
              <span>
                <strong>Aisha Rahman</strong> <small>Workspace admin</small>
              </span>
              <ChevronDown className="sp-icon" aria-hidden="true" />
            </>
          )}
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent side="right" align="end">
        <DropdownMenuItem onSelect={onSettings}>
          Workspace settings
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
function WorkspaceSidebar({
  page,
  navigate,
  projectCount,
  helpId,
}: {
  page: Page;
  navigate: (page: Page) => void;
  projectCount: number;
  helpId: string;
}) {
  return (
    <AdminSidebar
      aria-label="Workspace navigation"
      brand={WorkspaceBrand}
      footer={({ compact }) => (
        <WorkspaceAccount
          compact={compact}
          onSettings={() => navigate('settings')}
        />
      )}
    >
      <AdminSidebarSection title="Workspace">
        <AdminSidebarLink
          href="#overview"
          active={page === 'overview'}
          icon={<LayoutDashboard className="sp-icon" />}
          onClick={() => navigate('overview')}
        >
          Overview
        </AdminSidebarLink>
        <AdminSidebarLink
          href="#projects"
          active={page === 'projects'}
          badge={projectCount}
          icon={<FolderKanban className="sp-icon" />}
          onClick={() => navigate('projects')}
        >
          Projects
        </AdminSidebarLink>
        <AdminSidebarGroup
          label="Library"
          icon={<Archive className="sp-icon" />}
          active={page === 'archived'}
        >
          <AdminSidebarLink
            href="#all-projects"
            onClick={() => navigate('projects')}
          >
            All projects
          </AdminSidebarLink>
          <AdminSidebarLink
            href="#archived-projects"
            active={page === 'archived'}
            onClick={() => navigate('archived')}
          >
            Archived projects
          </AdminSidebarLink>
        </AdminSidebarGroup>
      </AdminSidebarSection>
      <AdminSidebarSection title="Manage">
        <AdminSidebarLink
          href="#settings"
          active={page === 'settings'}
          icon={<Settings className="sp-icon" />}
          onClick={() => navigate('settings')}
        >
          Settings
        </AdminSidebarLink>
        <AdminSidebarLink href={`#${helpId}`}>Help</AdminSidebarLink>
      </AdminSidebarSection>
    </AdminSidebar>
  );
}
function WorkspaceOverview({
  projects,
  actions,
  navigate,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  navigate: (page: Page) => void;
}) {
  return (
    <>
      <PageHeader
        title="Workspace overview"
        description="Your projects, priorities and recent activity in one place."
        actions={actions}
      />
      <StatGroup cols={3}>
        <Stat
          label="Active projects"
          value={
            projects.filter((project) => project.status === 'Active').length
          }
          description="Across three clients"
        />
        <Stat
          label="Awaiting review"
          value="2"
          description="Ready for your feedback"
        />
        <Stat
          label="Team members"
          value="8"
          description="3 collaborators online"
        />
      </StatGroup>
      <div className="demo-workspace-overview">
        <Card padding="none">
          <div className="demo-workspace-card-heading">
            <h2>Recent projects</h2>
            <Button
              variant="ghost"
              size="sm"
              onClick={() => navigate('projects')}
            >
              View all projects
            </Button>
          </div>
          <ProjectTable projects={projects.slice(0, 4)} />
        </Card>
        <Card>
          <h2>Recent activity</h2>
          <ol className="demo-workspace-activity">
            <li>
              <strong>Customer portal updated</strong>
              <span>Aisha added the onboarding flow.</span>
              <small>12 minutes ago</small>
            </li>
            <li>
              <strong>Dashboard ready for review</strong>
              <span>James shared the latest version.</span>
              <small>1 hour ago</small>
            </li>
            <li>
              <strong>New teammate joined</strong>
              <span>Sarah joined the design team.</span>
              <small>Today, 09:15</small>
            </li>
          </ol>
        </Card>
      </div>
    </>
  );
}
function WorkspaceSettings() {
  const [name, setName] = useState('Sapphire');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(false);
  return (
    <>
      <PageHeader
        title="Workspace settings"
        description="Manage the details your team shares."
      />
      <Card className="demo-workspace-settings">
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            setError(name.trim() ? '' : 'Enter a workspace name.');
            setSaved(Boolean(name.trim()));
          }}
        >
          <FormSection
            legend="General"
            description="These details appear throughout your workspace."
          >
            <FormField
              label="Workspace name"
              required
              error={error}
              hint="Choose a name your team will recognize."
            >
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setSaved(false);
                }}
              />
            </FormField>
            <FormField label="Timezone">
              <NativeSelect
                defaultValue="Asia/Kuala_Lumpur"
                name="timezone"
                onChange={() => setSaved(false)}
              >
                <option value="Asia/Kuala_Lumpur">Kuala Lumpur (UTC+8)</option>
                <option value="Europe/London">London</option>
              </NativeSelect>
            </FormField>
          </FormSection>
          <FormSection
            legend="Notifications"
            description="Choose what reaches your inbox."
          >
            <Checkbox
              label="Weekly project summary"
              description="A digest of progress and upcoming reviews."
              defaultChecked
              name="weekly"
              onCheckedChange={() => setSaved(false)}
            />
            <Checkbox
              label="Project review requests"
              description="An email when a teammate needs your feedback."
              defaultChecked
              name="reviews"
              onCheckedChange={() => setSaved(false)}
            />
          </FormSection>
          <div className="demo-workspace-save">
            <Button type="submit">Save changes</Button>
            <span role="status">
              {saved ? 'Workspace settings saved.' : ''}
            </span>
          </div>
        </form>
      </Card>
    </>
  );
}
function WorkspaceDemo({
  initialPage = 'overview',
  compact = false,
}: {
  initialPage?: Page;
  compact?: boolean;
}) {
  const [page, setPage] = useState(initialPage);
  const helpId = useId();
  const [projects, setProjects] = useState<readonly Project[]>(initialProjects);
  const actions = (
    <NewProject
      onCreate={(project) => {
        setProjects((current) => [project, ...current]);
        setPage('projects');
      }}
    />
  );
  return (
    <AdminLayout
      defaultDesktopState={compact ? 'compact' : 'expanded'}
      sidebar={
        <WorkspaceSidebar
          page={page}
          navigate={setPage}
          projectCount={projects.length}
          helpId={helpId}
        />
      }
      header={
        <AdminHeader
          leftSlot={
            <strong className="demo-workspace-app-name">Sapphire</strong>
          }
          breadcrumbs={<Breadcrumb items={[{ label: titles[page] }]} />}
          rightSlot={<ThemeToggle />}
        />
      }
    >
      {page === 'overview' && (
        <WorkspaceOverview
          projects={projects}
          actions={actions}
          navigate={setPage}
        />
      )}
      {(page === 'projects' || page === 'archived') && (
        <WorkspaceProjects
          key={page}
          projects={projects}
          actions={actions}
          archived={page === 'archived'}
        />
      )}
      {page === 'settings' && <WorkspaceSettings />}
      <p id={helpId} className="demo-workspace-help">
        Need a hand? Contact your workspace administrator.
      </p>
    </AdminLayout>
  );
}
function AdminWorkspace() {
  return <WorkspaceDemo />;
}

export default AdminWorkspace;
Example CSS

Load these demo styles alongside the package stylesheet for the same layout and surfaces.

.docs-admin-demo {
  height: 85vh;
  min-height: calc(var(--space-1) * 175);
  margin: 0 calc(var(--space-8) * -1) var(--space-12);
  overflow: hidden;
  border-block: var(--border-width) solid var(--border);
}

.docs-admin-demo[data-short] {
  height: calc(var(--space-1) * 100);
  min-height: 0;
}

.docs-admin-shell {
  height: 100%;
}

.docs-admin-brand {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  padding-inline-start: var(--space-1);
}

.docs-admin-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-6);
  height: var(--space-6);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: calc(1.25 / 0.875);
  color: var(--brand-navy);
  background: var(--brand);
  border-radius: var(--radius-sm);
}

.docs-admin-brand-name {
  font-family: var(--font-serif);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.5;
  color: var(--foreground);
  letter-spacing: 0.025em;
}

.docs-admin-brand-suffix {
  font-family: var(--font-sans);
  font-size: calc(var(--text-base) * 0.625);
  color: var(--brand-text);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.docs-admin-user {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.docs-admin-user-name {
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: calc(1 / 0.75);
  color: var(--foreground);
}

.docs-admin-user-role {
  font-size: calc(var(--text-base) * 0.625);
  color: var(--muted-foreground);
}

.docs-admin-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.docs-admin-avatar-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  cursor: pointer;
  background: transparent;
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.docs-admin-banner {
  margin-bottom: var(--space-6);
}

.docs-admin-stats {
  margin-bottom: var(--space-8);
}

.docs-admin-card-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.docs-admin-card-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: calc(1.75 / 1.125);
  color: var(--foreground-strong);
}

.docs-admin-card-text {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  line-height: calc(1.25 / 0.875);
  color: var(--muted-foreground);
}

.docs-admin-card-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

@media (min-width: 40rem) {
  .docs-admin-card-head {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

.docs-admin-parts {
  max-width: calc(var(--space-1) * 200);
  margin-bottom: var(--space-12);
}

.docs-admin-parts-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-6);
}

.docs-admin-glyph {
  --docs-admin-glyph-size: calc(var(--space-1) * 4.5);

  display: inline-block;
  width: var(--docs-admin-glyph-size);
  height: var(--docs-admin-glyph-size);
}

.docs-admin-glyph[data-size="sm"] {
  --docs-admin-glyph-size: var(--space-4);
}

.docs-admin-glyph[data-size="lg"] {
  --docs-admin-glyph-size: var(--space-5);
}

.docs-admin-glyph[data-size="xl"] {
  --docs-admin-glyph-size: var(--space-6);
}

.demo-workspace-identity {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  width: 100%;
  min-width: 0;
  color: var(--foreground);
}

.demo-workspace-identity > span:not(:first-child) {
  min-width: 0;
}

.demo-workspace-identity strong,
.demo-workspace-identity small {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.demo-workspace-identity strong {
  font-size: var(--text-sm);
  font-weight: 600;
}

.demo-workspace-identity small {
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-workspace-mark,
.demo-workspace-avatar {
  display: inline-grid;
  flex-shrink: 0;
  place-items: center;
  width: var(--space-8);
  height: var(--space-8);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  color: var(--foreground);
  background: var(--muted);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.demo-workspace-mark {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  color: var(--on-dark);
  background: var(--brand-navy);
  border-radius: var(--radius-btn);
}

.demo-workspace-account {
  min-height: calc(var(--space-1) * 11);
  padding: var(--space-1);
  text-align: start;
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-workspace-account:hover {
  background: var(--accent);
}

.demo-workspace-account > svg {
  margin-inline-start: auto;
}

.demo-workspace-app-name {
  font-size: var(--text-sm);
}

.demo-workspace-overview {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  margin-top: var(--space-6);
}

.demo-workspace-overview h2 {
  margin: 0;
  font-size: var(--text-base);
}

.demo-workspace-card-heading,
.demo-workspace-filters,
.demo-workspace-pagination,
.demo-workspace-save {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
}

.demo-workspace-filters {
  justify-content: start;
}

.demo-workspace-filters .sp-input {
  flex: 1;
  min-width: 0;
  max-width: calc(var(--space-16) * 5);
}

.demo-workspace-filters .sp-native-select {
  width: auto;
}

.demo-workspace-pagination {
  font-size: var(--text-sm);
  color: var(--muted-foreground);
  border-top: var(--border-width) solid var(--border);
}

.demo-project-client {
  display: block;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-project-updated {
  white-space: nowrap;
}

.demo-workspace-activity {
  display: grid;
  gap: var(--space-5);
  padding: 0;
  margin: var(--space-5) 0 0;
  list-style: none;
}

.demo-workspace-activity strong,
.demo-workspace-activity span,
.demo-workspace-activity small {
  display: block;
  font-size: var(--text-sm);
}

.demo-workspace-activity span,
.demo-workspace-activity small {
  color: var(--muted-foreground);
}

.demo-workspace-activity small {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
}

.demo-workspace-settings {
  max-width: calc(var(--space-16) * 10);
}

.demo-workspace-save {
  justify-content: start;
  padding: var(--space-6) 0 0;
  margin-top: var(--space-6);
  font-size: var(--text-sm);
  border-top: var(--border-width) solid var(--border);
}

.demo-workspace-help {
  margin: var(--space-6) 0 0;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

@media (max-width: 63.999rem) {
  .demo-workspace-overview {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 47.999rem) {
  .demo-workspace-app-name {
    display: none;
  }

  .demo-workspace-filters .sp-input {
    flex-basis: 100%;
    max-width: none;
  }
}

.demo-admin-icon-button {
  display: grid;
  place-items: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-admin-doc-title {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.demo-admin-doc-icon {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: calc(var(--space-1) * 10);
  height: calc(var(--space-1) * 10);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

.demo-admin-doc-icon[data-kind="plan"] {
  color: var(--foreground-strong);
  background: color-mix(in srgb, var(--foreground-strong) 4%, transparent);
}

.demo-admin-doc-icon[data-kind="proposal"] {
  color: var(--brand-text);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border-color: color-mix(in srgb, var(--brand) 30%, transparent);
}

.demo-admin-doc-title b {
  display: block;
  font-weight: 600;
  line-height: 1.25;
  color: var(--foreground);
}

.demo-admin-doc-title small {
  display: block;
  margin-top: calc(var(--space-1) * 0.75);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-row-actions {
  display: flex;
  gap: calc(var(--space-1) * 0.5);
  justify-content: flex-end;
}

.demo-admin-row-actions button,
.demo-admin-pass button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1);
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
}

.demo-admin-pass {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-pass-dots {
  letter-spacing: 0.12em;
}

.demo-admin-search {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  width: min(calc(var(--space-1) * 105), 100%);
  padding: var(--space-2) calc(var(--space-1) * 2.5);
  color: var(--muted-foreground);
  background: var(--background);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-btn);
}

.demo-admin-search:focus-within {
  box-shadow: 0 0 0 var(--ring-width) var(--ring);
}

.demo-admin-search input {
  flex: 1;
  min-width: 0;
  padding: 0;
  font: inherit;
  color: var(--foreground);
  background: transparent;
  border: 0;
  outline: 0;
}

.demo-admin-search input::placeholder {
  color: color-mix(in oklab, currentcolor 65%, transparent);
  opacity: 1;
}

@media (max-width: 50rem) {
  .demo-admin-search {
    width: 100%;
  }
}
HTML markup

Initial rendered HTML only. React handlers and portalled content require the JSX example; static HTML does not reproduce those interactions.

Collapsed

JSX usage

Install this example’s dependencies, load the package stylesheet from Setup, and add Example CSS where shown. The snippet includes its local helpers.

bun add @kongmy-dev/sapphire-ui react react-dom lucide-react radix-ui
"use client";
import { Plus } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Badge } from "@kongmy-dev/sapphire-ui/badge";
import { Button } from "@kongmy-dev/sapphire-ui/button";
import { Card } from "@kongmy-dev/sapphire-ui/card";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@kongmy-dev/sapphire-ui/dialog";
import { Empty } from "@kongmy-dev/sapphire-ui/empty";
import { FormField } from "@kongmy-dev/sapphire-ui/form-field";
import { Input } from "@kongmy-dev/sapphire-ui/input";
import { NativeSelect } from "@kongmy-dev/sapphire-ui/native-select";
import { PageHeader } from "@kongmy-dev/sapphire-ui/page-header";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kongmy-dev/sapphire-ui/table";
import { Archive, ChevronDown, FolderKanban, LayoutDashboard, Settings } from "lucide-react";
import { useId } from "react";
import { Breadcrumb } from "@kongmy-dev/sapphire-ui/breadcrumb";
import { Checkbox } from "@kongmy-dev/sapphire-ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@kongmy-dev/sapphire-ui/dropdown-menu";
import { FormSection } from "@kongmy-dev/sapphire-ui/form-section";
import { Stat } from "@kongmy-dev/sapphire-ui/stat";
import { StatGroup } from "@kongmy-dev/sapphire-ui/stat-group";
import { ThemeToggle } from "@kongmy-dev/sapphire-ui/theme-toggle";
import { AdminHeader, AdminLayout, AdminSidebar, AdminSidebarGroup, AdminSidebarLink, AdminSidebarSection } from "@kongmy-dev/sapphire-ui/admin-layout";

export const initialProjects = [
  {
    name: 'Customer portal',
    client: 'Northstar',
    status: 'Active',
    updated: 'Today',
    owner: 'AR',
  },
  {
    name: 'Inventory refresh',
    client: 'Fieldwork',
    status: 'Active',
    updated: 'Today',
    owner: 'JL',
  },
  {
    name: 'Operations dashboard',
    client: 'Meridian',
    status: 'Review',
    updated: 'Yesterday',
    owner: 'AR',
  },
  {
    name: 'Brand website',
    client: 'Northstar',
    status: 'Active',
    updated: '22 Oct',
    owner: 'SK',
  },
  {
    name: 'Billing integration',
    client: 'Fieldwork',
    status: 'Review',
    updated: '21 Oct',
    owner: 'JL',
  },
  {
    name: 'Member directory',
    client: 'Meridian',
    status: 'Archived',
    updated: '18 Oct',
    owner: 'SK',
  },
] as const;
export type Project = {
  name: string;
  client: string;
  status: string;
  updated: string;
  owner: string;
};
export function NewProject({
  onCreate,
}: {
  onCreate: (project: Project) => void;
}) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  function changeOpen(next: boolean) {
    setOpen(next);
    if (!next) {
      setName('');
      setError('');
    }
  }
  return (
    <Dialog open={open} onOpenChange={changeOpen}>
      <DialogTrigger asChild>
        <Button icon={<Plus className="sp-icon" aria-hidden="true" />}>
          New project
        </Button>
      </DialogTrigger>
      <DialogContent>
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            if (!name.trim()) {
              setError('Enter a project name.');
              return;
            }
            onCreate({
              name: name.trim(),
              client: 'Your workspace',
              status: 'Active',
              updated: 'Just now',
              owner: 'AR',
            });
            changeOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Create project</DialogTitle>
            <DialogDescription>
              Start a project for your team.
            </DialogDescription>
          </DialogHeader>
          <DialogBody>
            <FormField label="Project name" required error={error}>
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setError('');
                }}
              />
            </FormField>
          </DialogBody>
          <DialogFooter>
            <Button
              type="button"
              variant="outline"
              onClick={() => changeOpen(false)}
            >
              Cancel
            </Button>
            <Button type="submit">Create project</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}
export function ProjectTable({ projects }: { projects: readonly Project[] }) {
  return (
    <Table aria-label="Projects">
      <TableHeader>
        <TableRow>
          <TableHead scope="col">Project</TableHead>
          <TableHead scope="col">Status</TableHead>
          <TableHead scope="col">Owner</TableHead>
          <TableHead scope="col">Updated</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {projects.map((project) => (
          <TableRow key={project.name}>
            <TableCell>
              <strong>{project.name}</strong>
              <span className="demo-project-client">{project.client}</span>
            </TableCell>
            <TableCell>
              <Badge
                variant={project.status === 'Active' ? 'success' : 'default'}
              >
                {project.status}
              </Badge>
            </TableCell>
            <TableCell>
              <span
                className="demo-workspace-avatar"
                role="img"
                aria-label={`Owner ${project.owner}`}
              >
                {project.owner}
              </span>
            </TableCell>
            <TableCell className="demo-project-updated">
              {project.updated}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
export function WorkspaceProjects({
  projects,
  actions,
  archived,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  archived: boolean;
}) {
  const [search, setSearch] = useState('');
  const [status, setStatus] = useState(archived ? 'Archived' : 'All');
  const [page, setPage] = useState(0);
  const filtered = projects.filter(
    (project) =>
      `${project.name} ${project.client}`
        .toLowerCase()
        .includes(search.toLowerCase()) &&
      (status === 'All' || project.status === status),
  );
  const pageCount = Math.max(1, Math.ceil(filtered.length / 4));
  const currentPage = Math.min(page, pageCount - 1);
  return (
    <>
      <PageHeader
        title={archived ? 'Archived projects' : 'Projects'}
        description="Find a project and see what needs attention."
        actions={actions}
      />
      <Card padding="none">
        <div className="demo-workspace-filters">
          <Input
            type="search"
            aria-label="Search projects"
            placeholder="Search projects or clients…"
            value={search}
            onChange={(event) => {
              setSearch(event.target.value);
              setPage(0);
            }}
          />
          <NativeSelect
            aria-label="Project status"
            value={status}
            onChange={(event) => {
              setStatus(event.target.value);
              setPage(0);
            }}
          >
            <option value="All">All statuses</option>
            <option>Active</option>
            <option>Review</option>
            <option>Archived</option>
          </NativeSelect>
        </div>
        {filtered.length ? (
          <ProjectTable
            projects={filtered.slice(currentPage * 4, currentPage * 4 + 4)}
          />
        ) : (
          <Empty
            title="No projects found"
            description="Try another search or clear your filters."
            action={
              <Button
                variant="outline"
                onClick={() => {
                  setSearch('');
                  setStatus('All');
                }}
              >
                Clear filters
              </Button>
            }
          />
        )}
        <ProjectPagination
          currentPage={currentPage}
          pageCount={pageCount}
          total={filtered.length}
          onPageChange={setPage}
        />
      </Card>
    </>
  );
}
function ProjectPagination({
  currentPage,
  pageCount,
  total,
  onPageChange,
}: {
  currentPage: number;
  pageCount: number;
  total: number;
  onPageChange: (page: number) => void;
}) {
  return (
    <div className="demo-workspace-pagination">
      <span role="status">
        {total} projects · Page {currentPage + 1} of {pageCount}
      </span>
      <div className="sp-cluster">
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage === 0}
          onClick={() => onPageChange(currentPage - 1)}
        >
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage + 1 === pageCount}
          onClick={() => onPageChange(currentPage + 1)}
        >
          Next
        </Button>
      </div>
    </div>
  );
}
type Page = 'overview' | 'projects' | 'archived' | 'settings';
const titles = {
  overview: 'Overview',
  projects: 'Projects',
  archived: 'Archived projects',
  settings: 'Workspace settings',
} as const;
function WorkspaceBrand({ compact }: { compact: boolean }) {
  return (
    <a
      className="demo-workspace-identity"
      href="#workspace"
      aria-label={compact ? 'Sapphire workspace' : 'S Sapphire Team workspace'}
    >
      <span className="demo-workspace-mark" aria-hidden="true">
        S
      </span>{' '}
      {!compact && (
        <span>
          <strong>Sapphire</strong> <small>Team workspace</small>
        </span>
      )}
    </a>
  );
}
function WorkspaceAccount({
  compact,
  onSettings,
}: {
  compact: boolean;
  onSettings: () => void;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          className="demo-workspace-identity demo-workspace-account"
          aria-label={
            compact
              ? 'AR Aisha Rahman account'
              : 'AR Aisha Rahman Workspace admin account'
          }
        >
          <span className="demo-workspace-avatar" aria-hidden="true">
            AR
          </span>{' '}
          {!compact && (
            <>
              <span>
                <strong>Aisha Rahman</strong> <small>Workspace admin</small>
              </span>
              <ChevronDown className="sp-icon" aria-hidden="true" />
            </>
          )}
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent side="right" align="end">
        <DropdownMenuItem onSelect={onSettings}>
          Workspace settings
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
function WorkspaceSidebar({
  page,
  navigate,
  projectCount,
  helpId,
}: {
  page: Page;
  navigate: (page: Page) => void;
  projectCount: number;
  helpId: string;
}) {
  return (
    <AdminSidebar
      aria-label="Workspace navigation"
      brand={WorkspaceBrand}
      footer={({ compact }) => (
        <WorkspaceAccount
          compact={compact}
          onSettings={() => navigate('settings')}
        />
      )}
    >
      <AdminSidebarSection title="Workspace">
        <AdminSidebarLink
          href="#overview"
          active={page === 'overview'}
          icon={<LayoutDashboard className="sp-icon" />}
          onClick={() => navigate('overview')}
        >
          Overview
        </AdminSidebarLink>
        <AdminSidebarLink
          href="#projects"
          active={page === 'projects'}
          badge={projectCount}
          icon={<FolderKanban className="sp-icon" />}
          onClick={() => navigate('projects')}
        >
          Projects
        </AdminSidebarLink>
        <AdminSidebarGroup
          label="Library"
          icon={<Archive className="sp-icon" />}
          active={page === 'archived'}
        >
          <AdminSidebarLink
            href="#all-projects"
            onClick={() => navigate('projects')}
          >
            All projects
          </AdminSidebarLink>
          <AdminSidebarLink
            href="#archived-projects"
            active={page === 'archived'}
            onClick={() => navigate('archived')}
          >
            Archived projects
          </AdminSidebarLink>
        </AdminSidebarGroup>
      </AdminSidebarSection>
      <AdminSidebarSection title="Manage">
        <AdminSidebarLink
          href="#settings"
          active={page === 'settings'}
          icon={<Settings className="sp-icon" />}
          onClick={() => navigate('settings')}
        >
          Settings
        </AdminSidebarLink>
        <AdminSidebarLink href={`#${helpId}`}>Help</AdminSidebarLink>
      </AdminSidebarSection>
    </AdminSidebar>
  );
}
function WorkspaceOverview({
  projects,
  actions,
  navigate,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  navigate: (page: Page) => void;
}) {
  return (
    <>
      <PageHeader
        title="Workspace overview"
        description="Your projects, priorities and recent activity in one place."
        actions={actions}
      />
      <StatGroup cols={3}>
        <Stat
          label="Active projects"
          value={
            projects.filter((project) => project.status === 'Active').length
          }
          description="Across three clients"
        />
        <Stat
          label="Awaiting review"
          value="2"
          description="Ready for your feedback"
        />
        <Stat
          label="Team members"
          value="8"
          description="3 collaborators online"
        />
      </StatGroup>
      <div className="demo-workspace-overview">
        <Card padding="none">
          <div className="demo-workspace-card-heading">
            <h2>Recent projects</h2>
            <Button
              variant="ghost"
              size="sm"
              onClick={() => navigate('projects')}
            >
              View all projects
            </Button>
          </div>
          <ProjectTable projects={projects.slice(0, 4)} />
        </Card>
        <Card>
          <h2>Recent activity</h2>
          <ol className="demo-workspace-activity">
            <li>
              <strong>Customer portal updated</strong>
              <span>Aisha added the onboarding flow.</span>
              <small>12 minutes ago</small>
            </li>
            <li>
              <strong>Dashboard ready for review</strong>
              <span>James shared the latest version.</span>
              <small>1 hour ago</small>
            </li>
            <li>
              <strong>New teammate joined</strong>
              <span>Sarah joined the design team.</span>
              <small>Today, 09:15</small>
            </li>
          </ol>
        </Card>
      </div>
    </>
  );
}
function WorkspaceSettings() {
  const [name, setName] = useState('Sapphire');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(false);
  return (
    <>
      <PageHeader
        title="Workspace settings"
        description="Manage the details your team shares."
      />
      <Card className="demo-workspace-settings">
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            setError(name.trim() ? '' : 'Enter a workspace name.');
            setSaved(Boolean(name.trim()));
          }}
        >
          <FormSection
            legend="General"
            description="These details appear throughout your workspace."
          >
            <FormField
              label="Workspace name"
              required
              error={error}
              hint="Choose a name your team will recognize."
            >
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setSaved(false);
                }}
              />
            </FormField>
            <FormField label="Timezone">
              <NativeSelect
                defaultValue="Asia/Kuala_Lumpur"
                name="timezone"
                onChange={() => setSaved(false)}
              >
                <option value="Asia/Kuala_Lumpur">Kuala Lumpur (UTC+8)</option>
                <option value="Europe/London">London</option>
              </NativeSelect>
            </FormField>
          </FormSection>
          <FormSection
            legend="Notifications"
            description="Choose what reaches your inbox."
          >
            <Checkbox
              label="Weekly project summary"
              description="A digest of progress and upcoming reviews."
              defaultChecked
              name="weekly"
              onCheckedChange={() => setSaved(false)}
            />
            <Checkbox
              label="Project review requests"
              description="An email when a teammate needs your feedback."
              defaultChecked
              name="reviews"
              onCheckedChange={() => setSaved(false)}
            />
          </FormSection>
          <div className="demo-workspace-save">
            <Button type="submit">Save changes</Button>
            <span role="status">
              {saved ? 'Workspace settings saved.' : ''}
            </span>
          </div>
        </form>
      </Card>
    </>
  );
}
function WorkspaceDemo({
  initialPage = 'overview',
  compact = false,
}: {
  initialPage?: Page;
  compact?: boolean;
}) {
  const [page, setPage] = useState(initialPage);
  const helpId = useId();
  const [projects, setProjects] = useState<readonly Project[]>(initialProjects);
  const actions = (
    <NewProject
      onCreate={(project) => {
        setProjects((current) => [project, ...current]);
        setPage('projects');
      }}
    />
  );
  return (
    <AdminLayout
      defaultDesktopState={compact ? 'compact' : 'expanded'}
      sidebar={
        <WorkspaceSidebar
          page={page}
          navigate={setPage}
          projectCount={projects.length}
          helpId={helpId}
        />
      }
      header={
        <AdminHeader
          leftSlot={
            <strong className="demo-workspace-app-name">Sapphire</strong>
          }
          breadcrumbs={<Breadcrumb items={[{ label: titles[page] }]} />}
          rightSlot={<ThemeToggle />}
        />
      }
    >
      {page === 'overview' && (
        <WorkspaceOverview
          projects={projects}
          actions={actions}
          navigate={setPage}
        />
      )}
      {(page === 'projects' || page === 'archived') && (
        <WorkspaceProjects
          key={page}
          projects={projects}
          actions={actions}
          archived={page === 'archived'}
        />
      )}
      {page === 'settings' && <WorkspaceSettings />}
      <p id={helpId} className="demo-workspace-help">
        Need a hand? Contact your workspace administrator.
      </p>
    </AdminLayout>
  );
}
function AdminCollapsed() {
  return <WorkspaceDemo compact initialPage="projects" />;
}

export default AdminCollapsed;
Example CSS

Load these demo styles alongside the package stylesheet for the same layout and surfaces.

.docs-admin-demo {
  height: 85vh;
  min-height: calc(var(--space-1) * 175);
  margin: 0 calc(var(--space-8) * -1) var(--space-12);
  overflow: hidden;
  border-block: var(--border-width) solid var(--border);
}

.docs-admin-demo[data-short] {
  height: calc(var(--space-1) * 100);
  min-height: 0;
}

.docs-admin-shell {
  height: 100%;
}

.docs-admin-brand {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  padding-inline-start: var(--space-1);
}

.docs-admin-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-6);
  height: var(--space-6);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: calc(1.25 / 0.875);
  color: var(--brand-navy);
  background: var(--brand);
  border-radius: var(--radius-sm);
}

.docs-admin-brand-name {
  font-family: var(--font-serif);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.5;
  color: var(--foreground);
  letter-spacing: 0.025em;
}

.docs-admin-brand-suffix {
  font-family: var(--font-sans);
  font-size: calc(var(--text-base) * 0.625);
  color: var(--brand-text);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.docs-admin-user {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.docs-admin-user-name {
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: calc(1 / 0.75);
  color: var(--foreground);
}

.docs-admin-user-role {
  font-size: calc(var(--text-base) * 0.625);
  color: var(--muted-foreground);
}

.docs-admin-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.docs-admin-avatar-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  cursor: pointer;
  background: transparent;
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.docs-admin-banner {
  margin-bottom: var(--space-6);
}

.docs-admin-stats {
  margin-bottom: var(--space-8);
}

.docs-admin-card-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.docs-admin-card-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: calc(1.75 / 1.125);
  color: var(--foreground-strong);
}

.docs-admin-card-text {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  line-height: calc(1.25 / 0.875);
  color: var(--muted-foreground);
}

.docs-admin-card-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

@media (min-width: 40rem) {
  .docs-admin-card-head {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

.docs-admin-parts {
  max-width: calc(var(--space-1) * 200);
  margin-bottom: var(--space-12);
}

.docs-admin-parts-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-6);
}

.docs-admin-glyph {
  --docs-admin-glyph-size: calc(var(--space-1) * 4.5);

  display: inline-block;
  width: var(--docs-admin-glyph-size);
  height: var(--docs-admin-glyph-size);
}

.docs-admin-glyph[data-size="sm"] {
  --docs-admin-glyph-size: var(--space-4);
}

.docs-admin-glyph[data-size="lg"] {
  --docs-admin-glyph-size: var(--space-5);
}

.docs-admin-glyph[data-size="xl"] {
  --docs-admin-glyph-size: var(--space-6);
}

.demo-workspace-identity {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  width: 100%;
  min-width: 0;
  color: var(--foreground);
}

.demo-workspace-identity > span:not(:first-child) {
  min-width: 0;
}

.demo-workspace-identity strong,
.demo-workspace-identity small {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.demo-workspace-identity strong {
  font-size: var(--text-sm);
  font-weight: 600;
}

.demo-workspace-identity small {
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-workspace-mark,
.demo-workspace-avatar {
  display: inline-grid;
  flex-shrink: 0;
  place-items: center;
  width: var(--space-8);
  height: var(--space-8);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  color: var(--foreground);
  background: var(--muted);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.demo-workspace-mark {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  color: var(--on-dark);
  background: var(--brand-navy);
  border-radius: var(--radius-btn);
}

.demo-workspace-account {
  min-height: calc(var(--space-1) * 11);
  padding: var(--space-1);
  text-align: start;
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-workspace-account:hover {
  background: var(--accent);
}

.demo-workspace-account > svg {
  margin-inline-start: auto;
}

.demo-workspace-app-name {
  font-size: var(--text-sm);
}

.demo-workspace-overview {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  margin-top: var(--space-6);
}

.demo-workspace-overview h2 {
  margin: 0;
  font-size: var(--text-base);
}

.demo-workspace-card-heading,
.demo-workspace-filters,
.demo-workspace-pagination,
.demo-workspace-save {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
}

.demo-workspace-filters {
  justify-content: start;
}

.demo-workspace-filters .sp-input {
  flex: 1;
  min-width: 0;
  max-width: calc(var(--space-16) * 5);
}

.demo-workspace-filters .sp-native-select {
  width: auto;
}

.demo-workspace-pagination {
  font-size: var(--text-sm);
  color: var(--muted-foreground);
  border-top: var(--border-width) solid var(--border);
}

.demo-project-client {
  display: block;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-project-updated {
  white-space: nowrap;
}

.demo-workspace-activity {
  display: grid;
  gap: var(--space-5);
  padding: 0;
  margin: var(--space-5) 0 0;
  list-style: none;
}

.demo-workspace-activity strong,
.demo-workspace-activity span,
.demo-workspace-activity small {
  display: block;
  font-size: var(--text-sm);
}

.demo-workspace-activity span,
.demo-workspace-activity small {
  color: var(--muted-foreground);
}

.demo-workspace-activity small {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
}

.demo-workspace-settings {
  max-width: calc(var(--space-16) * 10);
}

.demo-workspace-save {
  justify-content: start;
  padding: var(--space-6) 0 0;
  margin-top: var(--space-6);
  font-size: var(--text-sm);
  border-top: var(--border-width) solid var(--border);
}

.demo-workspace-help {
  margin: var(--space-6) 0 0;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

@media (max-width: 63.999rem) {
  .demo-workspace-overview {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 47.999rem) {
  .demo-workspace-app-name {
    display: none;
  }

  .demo-workspace-filters .sp-input {
    flex-basis: 100%;
    max-width: none;
  }
}

.demo-admin-icon-button {
  display: grid;
  place-items: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-admin-doc-title {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.demo-admin-doc-icon {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: calc(var(--space-1) * 10);
  height: calc(var(--space-1) * 10);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

.demo-admin-doc-icon[data-kind="plan"] {
  color: var(--foreground-strong);
  background: color-mix(in srgb, var(--foreground-strong) 4%, transparent);
}

.demo-admin-doc-icon[data-kind="proposal"] {
  color: var(--brand-text);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border-color: color-mix(in srgb, var(--brand) 30%, transparent);
}

.demo-admin-doc-title b {
  display: block;
  font-weight: 600;
  line-height: 1.25;
  color: var(--foreground);
}

.demo-admin-doc-title small {
  display: block;
  margin-top: calc(var(--space-1) * 0.75);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-row-actions {
  display: flex;
  gap: calc(var(--space-1) * 0.5);
  justify-content: flex-end;
}

.demo-admin-row-actions button,
.demo-admin-pass button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1);
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
}

.demo-admin-pass {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-pass-dots {
  letter-spacing: 0.12em;
}

.demo-admin-search {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  width: min(calc(var(--space-1) * 105), 100%);
  padding: var(--space-2) calc(var(--space-1) * 2.5);
  color: var(--muted-foreground);
  background: var(--background);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-btn);
}

.demo-admin-search:focus-within {
  box-shadow: 0 0 0 var(--ring-width) var(--ring);
}

.demo-admin-search input {
  flex: 1;
  min-width: 0;
  padding: 0;
  font: inherit;
  color: var(--foreground);
  background: transparent;
  border: 0;
  outline: 0;
}

.demo-admin-search input::placeholder {
  color: color-mix(in oklab, currentcolor 65%, transparent);
  opacity: 1;
}

@media (max-width: 50rem) {
  .demo-admin-search {
    width: 100%;
  }
}
HTML markup

Initial rendered HTML only. React handlers and portalled content require the JSX example; static HTML does not reproduce those interactions.

Records

JSX usage

Install this example’s dependencies, load the package stylesheet from Setup, and add Example CSS where shown. The snippet includes its local helpers.

bun add @kongmy-dev/sapphire-ui react react-dom lucide-react radix-ui
"use client";
import { Plus } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Badge } from "@kongmy-dev/sapphire-ui/badge";
import { Button } from "@kongmy-dev/sapphire-ui/button";
import { Card } from "@kongmy-dev/sapphire-ui/card";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@kongmy-dev/sapphire-ui/dialog";
import { Empty } from "@kongmy-dev/sapphire-ui/empty";
import { FormField } from "@kongmy-dev/sapphire-ui/form-field";
import { Input } from "@kongmy-dev/sapphire-ui/input";
import { NativeSelect } from "@kongmy-dev/sapphire-ui/native-select";
import { PageHeader } from "@kongmy-dev/sapphire-ui/page-header";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kongmy-dev/sapphire-ui/table";
import { Archive, ChevronDown, FolderKanban, LayoutDashboard, Settings } from "lucide-react";
import { useId } from "react";
import { Breadcrumb } from "@kongmy-dev/sapphire-ui/breadcrumb";
import { Checkbox } from "@kongmy-dev/sapphire-ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@kongmy-dev/sapphire-ui/dropdown-menu";
import { FormSection } from "@kongmy-dev/sapphire-ui/form-section";
import { Stat } from "@kongmy-dev/sapphire-ui/stat";
import { StatGroup } from "@kongmy-dev/sapphire-ui/stat-group";
import { ThemeToggle } from "@kongmy-dev/sapphire-ui/theme-toggle";
import { AdminHeader, AdminLayout, AdminSidebar, AdminSidebarGroup, AdminSidebarLink, AdminSidebarSection } from "@kongmy-dev/sapphire-ui/admin-layout";

export const initialProjects = [
  {
    name: 'Customer portal',
    client: 'Northstar',
    status: 'Active',
    updated: 'Today',
    owner: 'AR',
  },
  {
    name: 'Inventory refresh',
    client: 'Fieldwork',
    status: 'Active',
    updated: 'Today',
    owner: 'JL',
  },
  {
    name: 'Operations dashboard',
    client: 'Meridian',
    status: 'Review',
    updated: 'Yesterday',
    owner: 'AR',
  },
  {
    name: 'Brand website',
    client: 'Northstar',
    status: 'Active',
    updated: '22 Oct',
    owner: 'SK',
  },
  {
    name: 'Billing integration',
    client: 'Fieldwork',
    status: 'Review',
    updated: '21 Oct',
    owner: 'JL',
  },
  {
    name: 'Member directory',
    client: 'Meridian',
    status: 'Archived',
    updated: '18 Oct',
    owner: 'SK',
  },
] as const;
export type Project = {
  name: string;
  client: string;
  status: string;
  updated: string;
  owner: string;
};
export function NewProject({
  onCreate,
}: {
  onCreate: (project: Project) => void;
}) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  function changeOpen(next: boolean) {
    setOpen(next);
    if (!next) {
      setName('');
      setError('');
    }
  }
  return (
    <Dialog open={open} onOpenChange={changeOpen}>
      <DialogTrigger asChild>
        <Button icon={<Plus className="sp-icon" aria-hidden="true" />}>
          New project
        </Button>
      </DialogTrigger>
      <DialogContent>
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            if (!name.trim()) {
              setError('Enter a project name.');
              return;
            }
            onCreate({
              name: name.trim(),
              client: 'Your workspace',
              status: 'Active',
              updated: 'Just now',
              owner: 'AR',
            });
            changeOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Create project</DialogTitle>
            <DialogDescription>
              Start a project for your team.
            </DialogDescription>
          </DialogHeader>
          <DialogBody>
            <FormField label="Project name" required error={error}>
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setError('');
                }}
              />
            </FormField>
          </DialogBody>
          <DialogFooter>
            <Button
              type="button"
              variant="outline"
              onClick={() => changeOpen(false)}
            >
              Cancel
            </Button>
            <Button type="submit">Create project</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}
export function ProjectTable({ projects }: { projects: readonly Project[] }) {
  return (
    <Table aria-label="Projects">
      <TableHeader>
        <TableRow>
          <TableHead scope="col">Project</TableHead>
          <TableHead scope="col">Status</TableHead>
          <TableHead scope="col">Owner</TableHead>
          <TableHead scope="col">Updated</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {projects.map((project) => (
          <TableRow key={project.name}>
            <TableCell>
              <strong>{project.name}</strong>
              <span className="demo-project-client">{project.client}</span>
            </TableCell>
            <TableCell>
              <Badge
                variant={project.status === 'Active' ? 'success' : 'default'}
              >
                {project.status}
              </Badge>
            </TableCell>
            <TableCell>
              <span
                className="demo-workspace-avatar"
                role="img"
                aria-label={`Owner ${project.owner}`}
              >
                {project.owner}
              </span>
            </TableCell>
            <TableCell className="demo-project-updated">
              {project.updated}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
export function WorkspaceProjects({
  projects,
  actions,
  archived,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  archived: boolean;
}) {
  const [search, setSearch] = useState('');
  const [status, setStatus] = useState(archived ? 'Archived' : 'All');
  const [page, setPage] = useState(0);
  const filtered = projects.filter(
    (project) =>
      `${project.name} ${project.client}`
        .toLowerCase()
        .includes(search.toLowerCase()) &&
      (status === 'All' || project.status === status),
  );
  const pageCount = Math.max(1, Math.ceil(filtered.length / 4));
  const currentPage = Math.min(page, pageCount - 1);
  return (
    <>
      <PageHeader
        title={archived ? 'Archived projects' : 'Projects'}
        description="Find a project and see what needs attention."
        actions={actions}
      />
      <Card padding="none">
        <div className="demo-workspace-filters">
          <Input
            type="search"
            aria-label="Search projects"
            placeholder="Search projects or clients…"
            value={search}
            onChange={(event) => {
              setSearch(event.target.value);
              setPage(0);
            }}
          />
          <NativeSelect
            aria-label="Project status"
            value={status}
            onChange={(event) => {
              setStatus(event.target.value);
              setPage(0);
            }}
          >
            <option value="All">All statuses</option>
            <option>Active</option>
            <option>Review</option>
            <option>Archived</option>
          </NativeSelect>
        </div>
        {filtered.length ? (
          <ProjectTable
            projects={filtered.slice(currentPage * 4, currentPage * 4 + 4)}
          />
        ) : (
          <Empty
            title="No projects found"
            description="Try another search or clear your filters."
            action={
              <Button
                variant="outline"
                onClick={() => {
                  setSearch('');
                  setStatus('All');
                }}
              >
                Clear filters
              </Button>
            }
          />
        )}
        <ProjectPagination
          currentPage={currentPage}
          pageCount={pageCount}
          total={filtered.length}
          onPageChange={setPage}
        />
      </Card>
    </>
  );
}
function ProjectPagination({
  currentPage,
  pageCount,
  total,
  onPageChange,
}: {
  currentPage: number;
  pageCount: number;
  total: number;
  onPageChange: (page: number) => void;
}) {
  return (
    <div className="demo-workspace-pagination">
      <span role="status">
        {total} projects · Page {currentPage + 1} of {pageCount}
      </span>
      <div className="sp-cluster">
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage === 0}
          onClick={() => onPageChange(currentPage - 1)}
        >
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage + 1 === pageCount}
          onClick={() => onPageChange(currentPage + 1)}
        >
          Next
        </Button>
      </div>
    </div>
  );
}
type Page = 'overview' | 'projects' | 'archived' | 'settings';
const titles = {
  overview: 'Overview',
  projects: 'Projects',
  archived: 'Archived projects',
  settings: 'Workspace settings',
} as const;
function WorkspaceBrand({ compact }: { compact: boolean }) {
  return (
    <a
      className="demo-workspace-identity"
      href="#workspace"
      aria-label={compact ? 'Sapphire workspace' : 'S Sapphire Team workspace'}
    >
      <span className="demo-workspace-mark" aria-hidden="true">
        S
      </span>{' '}
      {!compact && (
        <span>
          <strong>Sapphire</strong> <small>Team workspace</small>
        </span>
      )}
    </a>
  );
}
function WorkspaceAccount({
  compact,
  onSettings,
}: {
  compact: boolean;
  onSettings: () => void;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          className="demo-workspace-identity demo-workspace-account"
          aria-label={
            compact
              ? 'AR Aisha Rahman account'
              : 'AR Aisha Rahman Workspace admin account'
          }
        >
          <span className="demo-workspace-avatar" aria-hidden="true">
            AR
          </span>{' '}
          {!compact && (
            <>
              <span>
                <strong>Aisha Rahman</strong> <small>Workspace admin</small>
              </span>
              <ChevronDown className="sp-icon" aria-hidden="true" />
            </>
          )}
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent side="right" align="end">
        <DropdownMenuItem onSelect={onSettings}>
          Workspace settings
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
function WorkspaceSidebar({
  page,
  navigate,
  projectCount,
  helpId,
}: {
  page: Page;
  navigate: (page: Page) => void;
  projectCount: number;
  helpId: string;
}) {
  return (
    <AdminSidebar
      aria-label="Workspace navigation"
      brand={WorkspaceBrand}
      footer={({ compact }) => (
        <WorkspaceAccount
          compact={compact}
          onSettings={() => navigate('settings')}
        />
      )}
    >
      <AdminSidebarSection title="Workspace">
        <AdminSidebarLink
          href="#overview"
          active={page === 'overview'}
          icon={<LayoutDashboard className="sp-icon" />}
          onClick={() => navigate('overview')}
        >
          Overview
        </AdminSidebarLink>
        <AdminSidebarLink
          href="#projects"
          active={page === 'projects'}
          badge={projectCount}
          icon={<FolderKanban className="sp-icon" />}
          onClick={() => navigate('projects')}
        >
          Projects
        </AdminSidebarLink>
        <AdminSidebarGroup
          label="Library"
          icon={<Archive className="sp-icon" />}
          active={page === 'archived'}
        >
          <AdminSidebarLink
            href="#all-projects"
            onClick={() => navigate('projects')}
          >
            All projects
          </AdminSidebarLink>
          <AdminSidebarLink
            href="#archived-projects"
            active={page === 'archived'}
            onClick={() => navigate('archived')}
          >
            Archived projects
          </AdminSidebarLink>
        </AdminSidebarGroup>
      </AdminSidebarSection>
      <AdminSidebarSection title="Manage">
        <AdminSidebarLink
          href="#settings"
          active={page === 'settings'}
          icon={<Settings className="sp-icon" />}
          onClick={() => navigate('settings')}
        >
          Settings
        </AdminSidebarLink>
        <AdminSidebarLink href={`#${helpId}`}>Help</AdminSidebarLink>
      </AdminSidebarSection>
    </AdminSidebar>
  );
}
function WorkspaceOverview({
  projects,
  actions,
  navigate,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  navigate: (page: Page) => void;
}) {
  return (
    <>
      <PageHeader
        title="Workspace overview"
        description="Your projects, priorities and recent activity in one place."
        actions={actions}
      />
      <StatGroup cols={3}>
        <Stat
          label="Active projects"
          value={
            projects.filter((project) => project.status === 'Active').length
          }
          description="Across three clients"
        />
        <Stat
          label="Awaiting review"
          value="2"
          description="Ready for your feedback"
        />
        <Stat
          label="Team members"
          value="8"
          description="3 collaborators online"
        />
      </StatGroup>
      <div className="demo-workspace-overview">
        <Card padding="none">
          <div className="demo-workspace-card-heading">
            <h2>Recent projects</h2>
            <Button
              variant="ghost"
              size="sm"
              onClick={() => navigate('projects')}
            >
              View all projects
            </Button>
          </div>
          <ProjectTable projects={projects.slice(0, 4)} />
        </Card>
        <Card>
          <h2>Recent activity</h2>
          <ol className="demo-workspace-activity">
            <li>
              <strong>Customer portal updated</strong>
              <span>Aisha added the onboarding flow.</span>
              <small>12 minutes ago</small>
            </li>
            <li>
              <strong>Dashboard ready for review</strong>
              <span>James shared the latest version.</span>
              <small>1 hour ago</small>
            </li>
            <li>
              <strong>New teammate joined</strong>
              <span>Sarah joined the design team.</span>
              <small>Today, 09:15</small>
            </li>
          </ol>
        </Card>
      </div>
    </>
  );
}
function WorkspaceSettings() {
  const [name, setName] = useState('Sapphire');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(false);
  return (
    <>
      <PageHeader
        title="Workspace settings"
        description="Manage the details your team shares."
      />
      <Card className="demo-workspace-settings">
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            setError(name.trim() ? '' : 'Enter a workspace name.');
            setSaved(Boolean(name.trim()));
          }}
        >
          <FormSection
            legend="General"
            description="These details appear throughout your workspace."
          >
            <FormField
              label="Workspace name"
              required
              error={error}
              hint="Choose a name your team will recognize."
            >
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setSaved(false);
                }}
              />
            </FormField>
            <FormField label="Timezone">
              <NativeSelect
                defaultValue="Asia/Kuala_Lumpur"
                name="timezone"
                onChange={() => setSaved(false)}
              >
                <option value="Asia/Kuala_Lumpur">Kuala Lumpur (UTC+8)</option>
                <option value="Europe/London">London</option>
              </NativeSelect>
            </FormField>
          </FormSection>
          <FormSection
            legend="Notifications"
            description="Choose what reaches your inbox."
          >
            <Checkbox
              label="Weekly project summary"
              description="A digest of progress and upcoming reviews."
              defaultChecked
              name="weekly"
              onCheckedChange={() => setSaved(false)}
            />
            <Checkbox
              label="Project review requests"
              description="An email when a teammate needs your feedback."
              defaultChecked
              name="reviews"
              onCheckedChange={() => setSaved(false)}
            />
          </FormSection>
          <div className="demo-workspace-save">
            <Button type="submit">Save changes</Button>
            <span role="status">
              {saved ? 'Workspace settings saved.' : ''}
            </span>
          </div>
        </form>
      </Card>
    </>
  );
}
function WorkspaceDemo({
  initialPage = 'overview',
  compact = false,
}: {
  initialPage?: Page;
  compact?: boolean;
}) {
  const [page, setPage] = useState(initialPage);
  const helpId = useId();
  const [projects, setProjects] = useState<readonly Project[]>(initialProjects);
  const actions = (
    <NewProject
      onCreate={(project) => {
        setProjects((current) => [project, ...current]);
        setPage('projects');
      }}
    />
  );
  return (
    <AdminLayout
      defaultDesktopState={compact ? 'compact' : 'expanded'}
      sidebar={
        <WorkspaceSidebar
          page={page}
          navigate={setPage}
          projectCount={projects.length}
          helpId={helpId}
        />
      }
      header={
        <AdminHeader
          leftSlot={
            <strong className="demo-workspace-app-name">Sapphire</strong>
          }
          breadcrumbs={<Breadcrumb items={[{ label: titles[page] }]} />}
          rightSlot={<ThemeToggle />}
        />
      }
    >
      {page === 'overview' && (
        <WorkspaceOverview
          projects={projects}
          actions={actions}
          navigate={setPage}
        />
      )}
      {(page === 'projects' || page === 'archived') && (
        <WorkspaceProjects
          key={page}
          projects={projects}
          actions={actions}
          archived={page === 'archived'}
        />
      )}
      {page === 'settings' && <WorkspaceSettings />}
      <p id={helpId} className="demo-workspace-help">
        Need a hand? Contact your workspace administrator.
      </p>
    </AdminLayout>
  );
}
function AdminRecords() {
  return <WorkspaceDemo initialPage="projects" />;
}

export default AdminRecords;
Example CSS

Load these demo styles alongside the package stylesheet for the same layout and surfaces.

.docs-admin-demo {
  height: 85vh;
  min-height: calc(var(--space-1) * 175);
  margin: 0 calc(var(--space-8) * -1) var(--space-12);
  overflow: hidden;
  border-block: var(--border-width) solid var(--border);
}

.docs-admin-demo[data-short] {
  height: calc(var(--space-1) * 100);
  min-height: 0;
}

.docs-admin-shell {
  height: 100%;
}

.docs-admin-brand {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  padding-inline-start: var(--space-1);
}

.docs-admin-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-6);
  height: var(--space-6);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: calc(1.25 / 0.875);
  color: var(--brand-navy);
  background: var(--brand);
  border-radius: var(--radius-sm);
}

.docs-admin-brand-name {
  font-family: var(--font-serif);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.5;
  color: var(--foreground);
  letter-spacing: 0.025em;
}

.docs-admin-brand-suffix {
  font-family: var(--font-sans);
  font-size: calc(var(--text-base) * 0.625);
  color: var(--brand-text);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.docs-admin-user {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.docs-admin-user-name {
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: calc(1 / 0.75);
  color: var(--foreground);
}

.docs-admin-user-role {
  font-size: calc(var(--text-base) * 0.625);
  color: var(--muted-foreground);
}

.docs-admin-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.docs-admin-avatar-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  cursor: pointer;
  background: transparent;
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.docs-admin-banner {
  margin-bottom: var(--space-6);
}

.docs-admin-stats {
  margin-bottom: var(--space-8);
}

.docs-admin-card-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.docs-admin-card-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: calc(1.75 / 1.125);
  color: var(--foreground-strong);
}

.docs-admin-card-text {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  line-height: calc(1.25 / 0.875);
  color: var(--muted-foreground);
}

.docs-admin-card-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

@media (min-width: 40rem) {
  .docs-admin-card-head {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

.docs-admin-parts {
  max-width: calc(var(--space-1) * 200);
  margin-bottom: var(--space-12);
}

.docs-admin-parts-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-6);
}

.docs-admin-glyph {
  --docs-admin-glyph-size: calc(var(--space-1) * 4.5);

  display: inline-block;
  width: var(--docs-admin-glyph-size);
  height: var(--docs-admin-glyph-size);
}

.docs-admin-glyph[data-size="sm"] {
  --docs-admin-glyph-size: var(--space-4);
}

.docs-admin-glyph[data-size="lg"] {
  --docs-admin-glyph-size: var(--space-5);
}

.docs-admin-glyph[data-size="xl"] {
  --docs-admin-glyph-size: var(--space-6);
}

.demo-workspace-identity {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  width: 100%;
  min-width: 0;
  color: var(--foreground);
}

.demo-workspace-identity > span:not(:first-child) {
  min-width: 0;
}

.demo-workspace-identity strong,
.demo-workspace-identity small {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.demo-workspace-identity strong {
  font-size: var(--text-sm);
  font-weight: 600;
}

.demo-workspace-identity small {
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-workspace-mark,
.demo-workspace-avatar {
  display: inline-grid;
  flex-shrink: 0;
  place-items: center;
  width: var(--space-8);
  height: var(--space-8);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  color: var(--foreground);
  background: var(--muted);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.demo-workspace-mark {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  color: var(--on-dark);
  background: var(--brand-navy);
  border-radius: var(--radius-btn);
}

.demo-workspace-account {
  min-height: calc(var(--space-1) * 11);
  padding: var(--space-1);
  text-align: start;
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-workspace-account:hover {
  background: var(--accent);
}

.demo-workspace-account > svg {
  margin-inline-start: auto;
}

.demo-workspace-app-name {
  font-size: var(--text-sm);
}

.demo-workspace-overview {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  margin-top: var(--space-6);
}

.demo-workspace-overview h2 {
  margin: 0;
  font-size: var(--text-base);
}

.demo-workspace-card-heading,
.demo-workspace-filters,
.demo-workspace-pagination,
.demo-workspace-save {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
}

.demo-workspace-filters {
  justify-content: start;
}

.demo-workspace-filters .sp-input {
  flex: 1;
  min-width: 0;
  max-width: calc(var(--space-16) * 5);
}

.demo-workspace-filters .sp-native-select {
  width: auto;
}

.demo-workspace-pagination {
  font-size: var(--text-sm);
  color: var(--muted-foreground);
  border-top: var(--border-width) solid var(--border);
}

.demo-project-client {
  display: block;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-project-updated {
  white-space: nowrap;
}

.demo-workspace-activity {
  display: grid;
  gap: var(--space-5);
  padding: 0;
  margin: var(--space-5) 0 0;
  list-style: none;
}

.demo-workspace-activity strong,
.demo-workspace-activity span,
.demo-workspace-activity small {
  display: block;
  font-size: var(--text-sm);
}

.demo-workspace-activity span,
.demo-workspace-activity small {
  color: var(--muted-foreground);
}

.demo-workspace-activity small {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
}

.demo-workspace-settings {
  max-width: calc(var(--space-16) * 10);
}

.demo-workspace-save {
  justify-content: start;
  padding: var(--space-6) 0 0;
  margin-top: var(--space-6);
  font-size: var(--text-sm);
  border-top: var(--border-width) solid var(--border);
}

.demo-workspace-help {
  margin: var(--space-6) 0 0;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

@media (max-width: 63.999rem) {
  .demo-workspace-overview {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 47.999rem) {
  .demo-workspace-app-name {
    display: none;
  }

  .demo-workspace-filters .sp-input {
    flex-basis: 100%;
    max-width: none;
  }
}

.demo-admin-icon-button {
  display: grid;
  place-items: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-admin-doc-title {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.demo-admin-doc-icon {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: calc(var(--space-1) * 10);
  height: calc(var(--space-1) * 10);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

.demo-admin-doc-icon[data-kind="plan"] {
  color: var(--foreground-strong);
  background: color-mix(in srgb, var(--foreground-strong) 4%, transparent);
}

.demo-admin-doc-icon[data-kind="proposal"] {
  color: var(--brand-text);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border-color: color-mix(in srgb, var(--brand) 30%, transparent);
}

.demo-admin-doc-title b {
  display: block;
  font-weight: 600;
  line-height: 1.25;
  color: var(--foreground);
}

.demo-admin-doc-title small {
  display: block;
  margin-top: calc(var(--space-1) * 0.75);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-row-actions {
  display: flex;
  gap: calc(var(--space-1) * 0.5);
  justify-content: flex-end;
}

.demo-admin-row-actions button,
.demo-admin-pass button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1);
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
}

.demo-admin-pass {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-pass-dots {
  letter-spacing: 0.12em;
}

.demo-admin-search {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  width: min(calc(var(--space-1) * 105), 100%);
  padding: var(--space-2) calc(var(--space-1) * 2.5);
  color: var(--muted-foreground);
  background: var(--background);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-btn);
}

.demo-admin-search:focus-within {
  box-shadow: 0 0 0 var(--ring-width) var(--ring);
}

.demo-admin-search input {
  flex: 1;
  min-width: 0;
  padding: 0;
  font: inherit;
  color: var(--foreground);
  background: transparent;
  border: 0;
  outline: 0;
}

.demo-admin-search input::placeholder {
  color: color-mix(in oklab, currentcolor 65%, transparent);
  opacity: 1;
}

@media (max-width: 50rem) {
  .demo-admin-search {
    width: 100%;
  }
}
HTML markup

Initial rendered HTML only. React handlers and portalled content require the JSX example; static HTML does not reproduce those interactions.

Settings

JSX usage

Install this example’s dependencies, load the package stylesheet from Setup, and add Example CSS where shown. The snippet includes its local helpers.

bun add @kongmy-dev/sapphire-ui react react-dom lucide-react radix-ui
"use client";
import { Plus } from "lucide-react";
import { type ReactNode, useState } from "react";
import { Badge } from "@kongmy-dev/sapphire-ui/badge";
import { Button } from "@kongmy-dev/sapphire-ui/button";
import { Card } from "@kongmy-dev/sapphire-ui/card";
import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@kongmy-dev/sapphire-ui/dialog";
import { Empty } from "@kongmy-dev/sapphire-ui/empty";
import { FormField } from "@kongmy-dev/sapphire-ui/form-field";
import { Input } from "@kongmy-dev/sapphire-ui/input";
import { NativeSelect } from "@kongmy-dev/sapphire-ui/native-select";
import { PageHeader } from "@kongmy-dev/sapphire-ui/page-header";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kongmy-dev/sapphire-ui/table";
import { Archive, ChevronDown, FolderKanban, LayoutDashboard, Settings } from "lucide-react";
import { useId } from "react";
import { Breadcrumb } from "@kongmy-dev/sapphire-ui/breadcrumb";
import { Checkbox } from "@kongmy-dev/sapphire-ui/checkbox";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@kongmy-dev/sapphire-ui/dropdown-menu";
import { FormSection } from "@kongmy-dev/sapphire-ui/form-section";
import { Stat } from "@kongmy-dev/sapphire-ui/stat";
import { StatGroup } from "@kongmy-dev/sapphire-ui/stat-group";
import { ThemeToggle } from "@kongmy-dev/sapphire-ui/theme-toggle";
import { AdminHeader, AdminLayout, AdminSidebar, AdminSidebarGroup, AdminSidebarLink, AdminSidebarSection } from "@kongmy-dev/sapphire-ui/admin-layout";

export const initialProjects = [
  {
    name: 'Customer portal',
    client: 'Northstar',
    status: 'Active',
    updated: 'Today',
    owner: 'AR',
  },
  {
    name: 'Inventory refresh',
    client: 'Fieldwork',
    status: 'Active',
    updated: 'Today',
    owner: 'JL',
  },
  {
    name: 'Operations dashboard',
    client: 'Meridian',
    status: 'Review',
    updated: 'Yesterday',
    owner: 'AR',
  },
  {
    name: 'Brand website',
    client: 'Northstar',
    status: 'Active',
    updated: '22 Oct',
    owner: 'SK',
  },
  {
    name: 'Billing integration',
    client: 'Fieldwork',
    status: 'Review',
    updated: '21 Oct',
    owner: 'JL',
  },
  {
    name: 'Member directory',
    client: 'Meridian',
    status: 'Archived',
    updated: '18 Oct',
    owner: 'SK',
  },
] as const;
export type Project = {
  name: string;
  client: string;
  status: string;
  updated: string;
  owner: string;
};
export function NewProject({
  onCreate,
}: {
  onCreate: (project: Project) => void;
}) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [error, setError] = useState('');
  function changeOpen(next: boolean) {
    setOpen(next);
    if (!next) {
      setName('');
      setError('');
    }
  }
  return (
    <Dialog open={open} onOpenChange={changeOpen}>
      <DialogTrigger asChild>
        <Button icon={<Plus className="sp-icon" aria-hidden="true" />}>
          New project
        </Button>
      </DialogTrigger>
      <DialogContent>
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            if (!name.trim()) {
              setError('Enter a project name.');
              return;
            }
            onCreate({
              name: name.trim(),
              client: 'Your workspace',
              status: 'Active',
              updated: 'Just now',
              owner: 'AR',
            });
            changeOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>Create project</DialogTitle>
            <DialogDescription>
              Start a project for your team.
            </DialogDescription>
          </DialogHeader>
          <DialogBody>
            <FormField label="Project name" required error={error}>
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setError('');
                }}
              />
            </FormField>
          </DialogBody>
          <DialogFooter>
            <Button
              type="button"
              variant="outline"
              onClick={() => changeOpen(false)}
            >
              Cancel
            </Button>
            <Button type="submit">Create project</Button>
          </DialogFooter>
        </form>
      </DialogContent>
    </Dialog>
  );
}
export function ProjectTable({ projects }: { projects: readonly Project[] }) {
  return (
    <Table aria-label="Projects">
      <TableHeader>
        <TableRow>
          <TableHead scope="col">Project</TableHead>
          <TableHead scope="col">Status</TableHead>
          <TableHead scope="col">Owner</TableHead>
          <TableHead scope="col">Updated</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {projects.map((project) => (
          <TableRow key={project.name}>
            <TableCell>
              <strong>{project.name}</strong>
              <span className="demo-project-client">{project.client}</span>
            </TableCell>
            <TableCell>
              <Badge
                variant={project.status === 'Active' ? 'success' : 'default'}
              >
                {project.status}
              </Badge>
            </TableCell>
            <TableCell>
              <span
                className="demo-workspace-avatar"
                role="img"
                aria-label={`Owner ${project.owner}`}
              >
                {project.owner}
              </span>
            </TableCell>
            <TableCell className="demo-project-updated">
              {project.updated}
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  );
}
export function WorkspaceProjects({
  projects,
  actions,
  archived,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  archived: boolean;
}) {
  const [search, setSearch] = useState('');
  const [status, setStatus] = useState(archived ? 'Archived' : 'All');
  const [page, setPage] = useState(0);
  const filtered = projects.filter(
    (project) =>
      `${project.name} ${project.client}`
        .toLowerCase()
        .includes(search.toLowerCase()) &&
      (status === 'All' || project.status === status),
  );
  const pageCount = Math.max(1, Math.ceil(filtered.length / 4));
  const currentPage = Math.min(page, pageCount - 1);
  return (
    <>
      <PageHeader
        title={archived ? 'Archived projects' : 'Projects'}
        description="Find a project and see what needs attention."
        actions={actions}
      />
      <Card padding="none">
        <div className="demo-workspace-filters">
          <Input
            type="search"
            aria-label="Search projects"
            placeholder="Search projects or clients…"
            value={search}
            onChange={(event) => {
              setSearch(event.target.value);
              setPage(0);
            }}
          />
          <NativeSelect
            aria-label="Project status"
            value={status}
            onChange={(event) => {
              setStatus(event.target.value);
              setPage(0);
            }}
          >
            <option value="All">All statuses</option>
            <option>Active</option>
            <option>Review</option>
            <option>Archived</option>
          </NativeSelect>
        </div>
        {filtered.length ? (
          <ProjectTable
            projects={filtered.slice(currentPage * 4, currentPage * 4 + 4)}
          />
        ) : (
          <Empty
            title="No projects found"
            description="Try another search or clear your filters."
            action={
              <Button
                variant="outline"
                onClick={() => {
                  setSearch('');
                  setStatus('All');
                }}
              >
                Clear filters
              </Button>
            }
          />
        )}
        <ProjectPagination
          currentPage={currentPage}
          pageCount={pageCount}
          total={filtered.length}
          onPageChange={setPage}
        />
      </Card>
    </>
  );
}
function ProjectPagination({
  currentPage,
  pageCount,
  total,
  onPageChange,
}: {
  currentPage: number;
  pageCount: number;
  total: number;
  onPageChange: (page: number) => void;
}) {
  return (
    <div className="demo-workspace-pagination">
      <span role="status">
        {total} projects · Page {currentPage + 1} of {pageCount}
      </span>
      <div className="sp-cluster">
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage === 0}
          onClick={() => onPageChange(currentPage - 1)}
        >
          Previous
        </Button>
        <Button
          variant="outline"
          size="sm"
          disabled={currentPage + 1 === pageCount}
          onClick={() => onPageChange(currentPage + 1)}
        >
          Next
        </Button>
      </div>
    </div>
  );
}
type Page = 'overview' | 'projects' | 'archived' | 'settings';
const titles = {
  overview: 'Overview',
  projects: 'Projects',
  archived: 'Archived projects',
  settings: 'Workspace settings',
} as const;
function WorkspaceBrand({ compact }: { compact: boolean }) {
  return (
    <a
      className="demo-workspace-identity"
      href="#workspace"
      aria-label={compact ? 'Sapphire workspace' : 'S Sapphire Team workspace'}
    >
      <span className="demo-workspace-mark" aria-hidden="true">
        S
      </span>{' '}
      {!compact && (
        <span>
          <strong>Sapphire</strong> <small>Team workspace</small>
        </span>
      )}
    </a>
  );
}
function WorkspaceAccount({
  compact,
  onSettings,
}: {
  compact: boolean;
  onSettings: () => void;
}) {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <button
          type="button"
          className="demo-workspace-identity demo-workspace-account"
          aria-label={
            compact
              ? 'AR Aisha Rahman account'
              : 'AR Aisha Rahman Workspace admin account'
          }
        >
          <span className="demo-workspace-avatar" aria-hidden="true">
            AR
          </span>{' '}
          {!compact && (
            <>
              <span>
                <strong>Aisha Rahman</strong> <small>Workspace admin</small>
              </span>
              <ChevronDown className="sp-icon" aria-hidden="true" />
            </>
          )}
        </button>
      </DropdownMenuTrigger>
      <DropdownMenuContent side="right" align="end">
        <DropdownMenuItem onSelect={onSettings}>
          Workspace settings
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
function WorkspaceSidebar({
  page,
  navigate,
  projectCount,
  helpId,
}: {
  page: Page;
  navigate: (page: Page) => void;
  projectCount: number;
  helpId: string;
}) {
  return (
    <AdminSidebar
      aria-label="Workspace navigation"
      brand={WorkspaceBrand}
      footer={({ compact }) => (
        <WorkspaceAccount
          compact={compact}
          onSettings={() => navigate('settings')}
        />
      )}
    >
      <AdminSidebarSection title="Workspace">
        <AdminSidebarLink
          href="#overview"
          active={page === 'overview'}
          icon={<LayoutDashboard className="sp-icon" />}
          onClick={() => navigate('overview')}
        >
          Overview
        </AdminSidebarLink>
        <AdminSidebarLink
          href="#projects"
          active={page === 'projects'}
          badge={projectCount}
          icon={<FolderKanban className="sp-icon" />}
          onClick={() => navigate('projects')}
        >
          Projects
        </AdminSidebarLink>
        <AdminSidebarGroup
          label="Library"
          icon={<Archive className="sp-icon" />}
          active={page === 'archived'}
        >
          <AdminSidebarLink
            href="#all-projects"
            onClick={() => navigate('projects')}
          >
            All projects
          </AdminSidebarLink>
          <AdminSidebarLink
            href="#archived-projects"
            active={page === 'archived'}
            onClick={() => navigate('archived')}
          >
            Archived projects
          </AdminSidebarLink>
        </AdminSidebarGroup>
      </AdminSidebarSection>
      <AdminSidebarSection title="Manage">
        <AdminSidebarLink
          href="#settings"
          active={page === 'settings'}
          icon={<Settings className="sp-icon" />}
          onClick={() => navigate('settings')}
        >
          Settings
        </AdminSidebarLink>
        <AdminSidebarLink href={`#${helpId}`}>Help</AdminSidebarLink>
      </AdminSidebarSection>
    </AdminSidebar>
  );
}
function WorkspaceOverview({
  projects,
  actions,
  navigate,
}: {
  projects: readonly Project[];
  actions: ReactNode;
  navigate: (page: Page) => void;
}) {
  return (
    <>
      <PageHeader
        title="Workspace overview"
        description="Your projects, priorities and recent activity in one place."
        actions={actions}
      />
      <StatGroup cols={3}>
        <Stat
          label="Active projects"
          value={
            projects.filter((project) => project.status === 'Active').length
          }
          description="Across three clients"
        />
        <Stat
          label="Awaiting review"
          value="2"
          description="Ready for your feedback"
        />
        <Stat
          label="Team members"
          value="8"
          description="3 collaborators online"
        />
      </StatGroup>
      <div className="demo-workspace-overview">
        <Card padding="none">
          <div className="demo-workspace-card-heading">
            <h2>Recent projects</h2>
            <Button
              variant="ghost"
              size="sm"
              onClick={() => navigate('projects')}
            >
              View all projects
            </Button>
          </div>
          <ProjectTable projects={projects.slice(0, 4)} />
        </Card>
        <Card>
          <h2>Recent activity</h2>
          <ol className="demo-workspace-activity">
            <li>
              <strong>Customer portal updated</strong>
              <span>Aisha added the onboarding flow.</span>
              <small>12 minutes ago</small>
            </li>
            <li>
              <strong>Dashboard ready for review</strong>
              <span>James shared the latest version.</span>
              <small>1 hour ago</small>
            </li>
            <li>
              <strong>New teammate joined</strong>
              <span>Sarah joined the design team.</span>
              <small>Today, 09:15</small>
            </li>
          </ol>
        </Card>
      </div>
    </>
  );
}
function WorkspaceSettings() {
  const [name, setName] = useState('Sapphire');
  const [error, setError] = useState('');
  const [saved, setSaved] = useState(false);
  return (
    <>
      <PageHeader
        title="Workspace settings"
        description="Manage the details your team shares."
      />
      <Card className="demo-workspace-settings">
        <form
          noValidate
          onSubmit={(event) => {
            event.preventDefault();
            setError(name.trim() ? '' : 'Enter a workspace name.');
            setSaved(Boolean(name.trim()));
          }}
        >
          <FormSection
            legend="General"
            description="These details appear throughout your workspace."
          >
            <FormField
              label="Workspace name"
              required
              error={error}
              hint="Choose a name your team will recognize."
            >
              <Input
                value={name}
                onChange={(event) => {
                  setName(event.target.value);
                  setSaved(false);
                }}
              />
            </FormField>
            <FormField label="Timezone">
              <NativeSelect
                defaultValue="Asia/Kuala_Lumpur"
                name="timezone"
                onChange={() => setSaved(false)}
              >
                <option value="Asia/Kuala_Lumpur">Kuala Lumpur (UTC+8)</option>
                <option value="Europe/London">London</option>
              </NativeSelect>
            </FormField>
          </FormSection>
          <FormSection
            legend="Notifications"
            description="Choose what reaches your inbox."
          >
            <Checkbox
              label="Weekly project summary"
              description="A digest of progress and upcoming reviews."
              defaultChecked
              name="weekly"
              onCheckedChange={() => setSaved(false)}
            />
            <Checkbox
              label="Project review requests"
              description="An email when a teammate needs your feedback."
              defaultChecked
              name="reviews"
              onCheckedChange={() => setSaved(false)}
            />
          </FormSection>
          <div className="demo-workspace-save">
            <Button type="submit">Save changes</Button>
            <span role="status">
              {saved ? 'Workspace settings saved.' : ''}
            </span>
          </div>
        </form>
      </Card>
    </>
  );
}
function WorkspaceDemo({
  initialPage = 'overview',
  compact = false,
}: {
  initialPage?: Page;
  compact?: boolean;
}) {
  const [page, setPage] = useState(initialPage);
  const helpId = useId();
  const [projects, setProjects] = useState<readonly Project[]>(initialProjects);
  const actions = (
    <NewProject
      onCreate={(project) => {
        setProjects((current) => [project, ...current]);
        setPage('projects');
      }}
    />
  );
  return (
    <AdminLayout
      defaultDesktopState={compact ? 'compact' : 'expanded'}
      sidebar={
        <WorkspaceSidebar
          page={page}
          navigate={setPage}
          projectCount={projects.length}
          helpId={helpId}
        />
      }
      header={
        <AdminHeader
          leftSlot={
            <strong className="demo-workspace-app-name">Sapphire</strong>
          }
          breadcrumbs={<Breadcrumb items={[{ label: titles[page] }]} />}
          rightSlot={<ThemeToggle />}
        />
      }
    >
      {page === 'overview' && (
        <WorkspaceOverview
          projects={projects}
          actions={actions}
          navigate={setPage}
        />
      )}
      {(page === 'projects' || page === 'archived') && (
        <WorkspaceProjects
          key={page}
          projects={projects}
          actions={actions}
          archived={page === 'archived'}
        />
      )}
      {page === 'settings' && <WorkspaceSettings />}
      <p id={helpId} className="demo-workspace-help">
        Need a hand? Contact your workspace administrator.
      </p>
    </AdminLayout>
  );
}
function AdminSettings() {
  return <WorkspaceDemo initialPage="settings" />;
}

export default AdminSettings;
Example CSS

Load these demo styles alongside the package stylesheet for the same layout and surfaces.

.docs-admin-demo {
  height: 85vh;
  min-height: calc(var(--space-1) * 175);
  margin: 0 calc(var(--space-8) * -1) var(--space-12);
  overflow: hidden;
  border-block: var(--border-width) solid var(--border);
}

.docs-admin-demo[data-short] {
  height: calc(var(--space-1) * 100);
  min-height: 0;
}

.docs-admin-shell {
  height: 100%;
}

.docs-admin-brand {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  padding-inline-start: var(--space-1);
}

.docs-admin-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--space-6);
  height: var(--space-6);
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: calc(1.25 / 0.875);
  color: var(--brand-navy);
  background: var(--brand);
  border-radius: var(--radius-sm);
}

.docs-admin-brand-name {
  font-family: var(--font-serif);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: 1.5;
  color: var(--foreground);
  letter-spacing: 0.025em;
}

.docs-admin-brand-suffix {
  font-family: var(--font-sans);
  font-size: calc(var(--text-base) * 0.625);
  color: var(--brand-text);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.docs-admin-user {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.docs-admin-user-name {
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: calc(1 / 0.75);
  color: var(--foreground);
}

.docs-admin-user-role {
  font-size: calc(var(--text-base) * 0.625);
  color: var(--muted-foreground);
}

.docs-admin-actions {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.docs-admin-avatar-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  cursor: pointer;
  background: transparent;
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.docs-admin-banner {
  margin-bottom: var(--space-6);
}

.docs-admin-stats {
  margin-bottom: var(--space-8);
}

.docs-admin-card-head {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.docs-admin-card-title {
  margin: 0;
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 500;
  line-height: calc(1.75 / 1.125);
  color: var(--foreground-strong);
}

.docs-admin-card-text {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  line-height: calc(1.25 / 0.875);
  color: var(--muted-foreground);
}

.docs-admin-card-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
}

@media (min-width: 40rem) {
  .docs-admin-card-head {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
  }
}

.docs-admin-parts {
  max-width: calc(var(--space-1) * 200);
  margin-bottom: var(--space-12);
}

.docs-admin-parts-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-6);
}

.docs-admin-glyph {
  --docs-admin-glyph-size: calc(var(--space-1) * 4.5);

  display: inline-block;
  width: var(--docs-admin-glyph-size);
  height: var(--docs-admin-glyph-size);
}

.docs-admin-glyph[data-size="sm"] {
  --docs-admin-glyph-size: var(--space-4);
}

.docs-admin-glyph[data-size="lg"] {
  --docs-admin-glyph-size: var(--space-5);
}

.docs-admin-glyph[data-size="xl"] {
  --docs-admin-glyph-size: var(--space-6);
}

.demo-workspace-identity {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  width: 100%;
  min-width: 0;
  color: var(--foreground);
}

.demo-workspace-identity > span:not(:first-child) {
  min-width: 0;
}

.demo-workspace-identity strong,
.demo-workspace-identity small {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.demo-workspace-identity strong {
  font-size: var(--text-sm);
  font-weight: 600;
}

.demo-workspace-identity small {
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-workspace-mark,
.demo-workspace-avatar {
  display: inline-grid;
  flex-shrink: 0;
  place-items: center;
  width: var(--space-8);
  height: var(--space-8);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1;
  color: var(--foreground);
  background: var(--muted);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
}

.demo-workspace-mark {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  color: var(--on-dark);
  background: var(--brand-navy);
  border-radius: var(--radius-btn);
}

.demo-workspace-account {
  min-height: calc(var(--space-1) * 11);
  padding: var(--space-1);
  text-align: start;
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-workspace-account:hover {
  background: var(--accent);
}

.demo-workspace-account > svg {
  margin-inline-start: auto;
}

.demo-workspace-app-name {
  font-size: var(--text-sm);
}

.demo-workspace-overview {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--space-6);
  margin-top: var(--space-6);
}

.demo-workspace-overview h2 {
  margin: 0;
  font-size: var(--text-base);
}

.demo-workspace-card-heading,
.demo-workspace-filters,
.demo-workspace-pagination,
.demo-workspace-save {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
}

.demo-workspace-filters {
  justify-content: start;
}

.demo-workspace-filters .sp-input {
  flex: 1;
  min-width: 0;
  max-width: calc(var(--space-16) * 5);
}

.demo-workspace-filters .sp-native-select {
  width: auto;
}

.demo-workspace-pagination {
  font-size: var(--text-sm);
  color: var(--muted-foreground);
  border-top: var(--border-width) solid var(--border);
}

.demo-project-client {
  display: block;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-project-updated {
  white-space: nowrap;
}

.demo-workspace-activity {
  display: grid;
  gap: var(--space-5);
  padding: 0;
  margin: var(--space-5) 0 0;
  list-style: none;
}

.demo-workspace-activity strong,
.demo-workspace-activity span,
.demo-workspace-activity small {
  display: block;
  font-size: var(--text-sm);
}

.demo-workspace-activity span,
.demo-workspace-activity small {
  color: var(--muted-foreground);
}

.demo-workspace-activity small {
  margin-top: var(--space-1);
  font-size: var(--text-xs);
}

.demo-workspace-settings {
  max-width: calc(var(--space-16) * 10);
}

.demo-workspace-save {
  justify-content: start;
  padding: var(--space-6) 0 0;
  margin-top: var(--space-6);
  font-size: var(--text-sm);
  border-top: var(--border-width) solid var(--border);
}

.demo-workspace-help {
  margin: var(--space-6) 0 0;
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

@media (max-width: 63.999rem) {
  .demo-workspace-overview {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 47.999rem) {
  .demo-workspace-app-name {
    display: none;
  }

  .demo-workspace-filters .sp-input {
    flex-basis: 100%;
    max-width: none;
  }
}

.demo-admin-icon-button {
  display: grid;
  place-items: center;
  width: calc(var(--space-1) * 9);
  height: calc(var(--space-1) * 9);
  padding: 0;
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-btn);
}

.demo-admin-doc-title {
  display: flex;
  gap: var(--space-3);
  align-items: center;
}

.demo-admin-doc-icon {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  width: calc(var(--space-1) * 10);
  height: calc(var(--space-1) * 10);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
}

.demo-admin-doc-icon[data-kind="plan"] {
  color: var(--foreground-strong);
  background: color-mix(in srgb, var(--foreground-strong) 4%, transparent);
}

.demo-admin-doc-icon[data-kind="proposal"] {
  color: var(--brand-text);
  background: color-mix(in srgb, var(--brand) 12%, transparent);
  border-color: color-mix(in srgb, var(--brand) 30%, transparent);
}

.demo-admin-doc-title b {
  display: block;
  font-weight: 600;
  line-height: 1.25;
  color: var(--foreground);
}

.demo-admin-doc-title small {
  display: block;
  margin-top: calc(var(--space-1) * 0.75);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-row-actions {
  display: flex;
  gap: calc(var(--space-1) * 0.5);
  justify-content: flex-end;
}

.demo-admin-row-actions button,
.demo-admin-pass button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-1);
  color: var(--muted-foreground);
  cursor: pointer;
  background: transparent;
  border: 0;
  border-radius: var(--radius-sm);
}

.demo-admin-pass {
  display: inline-flex;
  gap: var(--space-1);
  align-items: center;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--muted-foreground);
}

.demo-admin-pass-dots {
  letter-spacing: 0.12em;
}

.demo-admin-search {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  width: min(calc(var(--space-1) * 105), 100%);
  padding: var(--space-2) calc(var(--space-1) * 2.5);
  color: var(--muted-foreground);
  background: var(--background);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-btn);
}

.demo-admin-search:focus-within {
  box-shadow: 0 0 0 var(--ring-width) var(--ring);
}

.demo-admin-search input {
  flex: 1;
  min-width: 0;
  padding: 0;
  font: inherit;
  color: var(--foreground);
  background: transparent;
  border: 0;
  outline: 0;
}

.demo-admin-search input::placeholder {
  color: color-mix(in oklab, currentcolor 65%, transparent);
  opacity: 1;
}

@media (max-width: 50rem) {
  .demo-admin-search {
    width: 100%;
  }
}
HTML markup

Initial rendered HTML only. React handlers and portalled content require the JSX example; static HTML does not reproduce those interactions.

Props

Generated from the current public TypeScript signatures, including composed behaviour props. “—” means no default is declared in the local signature; forwarded behaviour defaults are not inferred.

AdminLayout

AdminLayout component props
PropTypeRequiredLocal default
childrenReactNodeYes—
desktopState"expanded" | "compact" | undefinedNo—
defaultDesktopState"expanded" | "compact" | undefinedNo—
onDesktopStateChange((state: "expanded" | "compact") => void) | undefinedNo—
mobileOpenboolean | undefinedNo—
defaultMobileOpenboolean | undefinedNo—
onMobileOpenChange((open: boolean) => void) | undefinedNo—
sidebarReactNodeYes—
headerReactNodeNo—
contentClassNamestring | undefinedNo—
contentMaxWidthstring | undefinedNo—
Inherited HTML / React props (279)
AdminLayout common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLDivElement> | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
onChangeimport("react").ChangeEventHandler<HTMLDivElement, Element> | undefinedNo—

All inherited prop names:

ref key defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex title translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

AdminSidebar

AdminSidebar component props
PropTypeRequiredLocal default
brandSidebarContentNo—
footerSidebarContentNo—
closeLabelstring | undefinedNo'Close navigation'
Inherited HTML / React props (280)
AdminSidebar common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLElement> | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
childrenReactNodeNo—
onChangeimport("react").ChangeEventHandler<HTMLElement, Element> | undefinedNo—

All inherited prop names:

ref key defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex title translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext children dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

AdminSidebarTrigger

No additional Sapphire props; accepts the platform props below.

Inherited HTML / React props (290)
AdminSidebarTrigger common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLButtonElement> | undefinedNo—
disabledboolean | undefinedNo—
namestring | undefinedNo—
type"button" | "submit" | "reset" | undefinedNo—
valuestring | number | readonly string[] | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
childrenReactNodeNo—
onChangeimport("react").ChangeEventHandler<HTMLButtonElement, Element> | undefinedNo—

All inherited prop names:

ref key disabled form formAction formEncType formMethod formNoValidate formTarget name type value defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex title translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext children dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

AdminHeader

AdminHeader component props
PropTypeRequiredLocal default
leftSlotReactNodeNo—
breadcrumbsReactNodeNo—
rightSlotReactNodeNo—
Inherited HTML / React props (280)
AdminHeader common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLElement> | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
childrenReactNodeNo—
onChangeimport("react").ChangeEventHandler<HTMLElement, Element> | undefinedNo—

All inherited prop names:

ref key defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex title translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext children dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

AdminSidebarGroup

AdminSidebarGroup component props
PropTypeRequiredLocal default
childrenReactNodeYes—
labelstringYes—
iconimport("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null | undefinedNo—
activeboolean | undefinedNofalse
Inherited HTML / React props (279)
AdminSidebarGroup common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLDivElement> | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
onChangeimport("react").ChangeEventHandler<HTMLDivElement, Element> | undefinedNo—

All inherited prop names:

ref key defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex title translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

AdminSidebarLink

AdminSidebarLink component props
PropTypeRequiredLocal default
asChildboolean | undefinedNofalse
iconimport("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | null | undefinedNo—
activeboolean | undefinedNofalse
badgeReactNodeNo—
Inherited HTML / React props (288)
AdminSidebarLink common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLAnchorElement> | undefinedNo—
hrefstring | undefinedNo—
typestring | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
childrenReactNodeNo—
onChangeimport("react").ChangeEventHandler<HTMLAnchorElement, Element> | undefinedNo—

All inherited prop names:

ref key download href hrefLang media ping target type referrerPolicy defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex title translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext children dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

AdminSidebarSection

AdminSidebarSection component props
PropTypeRequiredLocal default
titlestring | undefinedNo—
childrenReactNodeYes—
Inherited HTML / React props (278)
AdminSidebarSection common platform props
PropTypeRequiredLocal default
refimport("react").Ref<HTMLDivElement> | undefinedNo—
defaultValuestring | number | readonly string[] | undefinedNo—
classNamestring | undefinedNo—
idstring | undefinedNo—
styleimport("react").CSSProperties | undefinedNo—
tabIndexnumber | undefinedNo—
roleimport("react").AriaRole | undefinedNo—
aria-labelstring | undefinedNo—
onChangeimport("react").ChangeEventHandler<HTMLDivElement, Element> | undefinedNo—

All inherited prop names:

ref key defaultChecked defaultValue suppressContentEditableWarning suppressHydrationWarning accessKey autoCapitalize autoFocus className contentEditable contextMenu dir draggable enterKeyHint hidden id lang nonce slot spellCheck style tabIndex translate radioGroup role about content datatype inlist prefix property rel resource rev typeof vocab autoCorrect autoSave color itemProp itemScope itemType itemID itemRef results security unselectable popover popoverTargetAction popoverTarget inert inputMode is exportparts part aria-activedescendant aria-atomic aria-autocomplete aria-braillelabel aria-brailleroledescription aria-busy aria-checked aria-colcount aria-colindex aria-colindextext aria-colspan aria-controls aria-current aria-describedby aria-description aria-details aria-disabled aria-dropeffect aria-errormessage aria-expanded aria-flowto aria-grabbed aria-haspopup aria-hidden aria-invalid aria-keyshortcuts aria-label aria-labelledby aria-level aria-live aria-modal aria-multiline aria-multiselectable aria-orientation aria-owns aria-placeholder aria-posinset aria-pressed aria-readonly aria-relevant aria-required aria-roledescription aria-rowcount aria-rowindex aria-rowindextext aria-rowspan aria-selected aria-setsize aria-sort aria-valuemax aria-valuemin aria-valuenow aria-valuetext dangerouslySetInnerHTML onCopy onCopyCapture onCut onCutCapture onPaste onPasteCapture onCompositionEnd onCompositionEndCapture onCompositionStart onCompositionStartCapture onCompositionUpdate onCompositionUpdateCapture onFocus onFocusCapture onBlur onBlurCapture onChange onChangeCapture onBeforeInput onBeforeInputCapture onInput onInputCapture onReset onResetCapture onSubmit onSubmitCapture onInvalid onInvalidCapture onLoad onLoadCapture onError onErrorCapture onKeyDown onKeyDownCapture onKeyPress onKeyPressCapture onKeyUp onKeyUpCapture onAbort onAbortCapture onCanPlay onCanPlayCapture onCanPlayThrough onCanPlayThroughCapture onDurationChange onDurationChangeCapture onEmptied onEmptiedCapture onEncrypted onEncryptedCapture onEnded onEndedCapture onLoadedData onLoadedDataCapture onLoadedMetadata onLoadedMetadataCapture onLoadStart onLoadStartCapture onPause onPauseCapture onPlay onPlayCapture onPlaying onPlayingCapture onProgress onProgressCapture onRateChange onRateChangeCapture onSeeked onSeekedCapture onSeeking onSeekingCapture onStalled onStalledCapture onSuspend onSuspendCapture onTimeUpdate onTimeUpdateCapture onVolumeChange onVolumeChangeCapture onWaiting onWaitingCapture onAuxClick onAuxClickCapture onClick onClickCapture onContextMenu onContextMenuCapture onDoubleClick onDoubleClickCapture onDrag onDragCapture onDragEnd onDragEndCapture onDragEnter onDragEnterCapture onDragExit onDragExitCapture onDragLeave onDragLeaveCapture onDragOver onDragOverCapture onDragStart onDragStartCapture onDrop onDropCapture onMouseDown onMouseDownCapture onMouseEnter onMouseLeave onMouseMove onMouseMoveCapture onMouseOut onMouseOutCapture onMouseOver onMouseOverCapture onMouseUp onMouseUpCapture onSelect onSelectCapture onTouchCancel onTouchCancelCapture onTouchEnd onTouchEndCapture onTouchMove onTouchMoveCapture onTouchStart onTouchStartCapture onPointerDown onPointerDownCapture onPointerMove onPointerMoveCapture onPointerUp onPointerUpCapture onPointerCancel onPointerCancelCapture onPointerEnter onPointerLeave onPointerOver onPointerOverCapture onPointerOut onPointerOutCapture onGotPointerCapture onGotPointerCaptureCapture onLostPointerCapture onLostPointerCaptureCapture onScroll onScrollCapture onScrollEnd onScrollEndCapture onWheel onWheelCapture onAnimationStart onAnimationStartCapture onAnimationEnd onAnimationEndCapture onAnimationIteration onAnimationIterationCapture onToggle onBeforeToggle onTransitionCancel onTransitionCancelCapture onTransitionEnd onTransitionEndCapture onTransitionRun onTransitionRunCapture onTransitionStart onTransitionStartCapture

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

SidebarContent

Supporting type used by this API.

import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>

No additional Sapphire props; accepts the platform props below.

Inherited HTML / React props (3)
SidebarContent common platform props
PropTypeRequiredLocal default
typestring | import("react").JSXElementConstructor<any>Yes—

All inherited prop names:

type props key

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

Iterable<ReactNode>

SidebarContent component props
PropTypeRequiredLocal default
__@iterator@88() => Iterator<ReactNode, any, any>Yes—

import("react").ReactPortal

No additional Sapphire props; accepts the platform props below.

Inherited HTML / React props (4)
SidebarContent common platform props
PropTypeRequiredLocal default
childrenReactNodeYes—
typestring | import("react").JSXElementConstructor<any>Yes—

All inherited prop names:

children type props key

ARIA attributes and native event handlers keep their React types. data-* attributes are also accepted by React.

Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined>

SidebarContent component props
PropTypeRequiredLocal default
then<TResult1 = string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined, TResult2 = never>(onfulfilled?: ((value: string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined) => TResult1 | PromiseLike<TResult1>) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined) => Promise<TResult1 | TResult2>Yes—
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined) => Promise<(string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined) | TResult>Yes—
finally(onfinally?: (() => void) | null | undefined) => Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined>Yes—
__@toStringTag@91stringYes—

Setup

bun add @kongmy-dev/sapphire-ui react react-dom radix-ui

For an app using package components throughout, load the full stylesheet once. Add this component import where you use it.

import "@kongmy-dev/sapphire-ui/sapphire.css";
import { AdminLayout } from "@kongmy-dev/sapphire-ui/admin-layout";

Mixing package and vendored components? Follow application setup.

Search documentation