Surface System

Context-aware surface nesting, shadows, and app integration.

The surface system is how Cubitt keeps nested UI from flattening into the same background. Consumers declare the surface level only where the app paints its own boundary. Cubitt components then move relative to that level and pass the computed level to their children.

Surface Levels

Use odd-numbered levels for major surfaces and even-numbered levels for interaction states.

LevelRole
surface-1Main product inset or full-screen workspace
surface-2Hover, focus, or row state on surface-1
surface-3App shell, sidebar, page chrome, or card above surface-1
surface-4Hover, focus, or row state on surface-3
surface-5Popover, panel, or floating content above surface-3
surface-6Hover, focus, or row state on surface-5
surface-7Menu or deeply nested floating content
surface-8Hover, focus, or row state on surface-7

Ownership Model

Consumers set surface context only at app-owned painted boundaries. If the app shell is built with Cubitt's sidebar components, SidebarProvider is that painted boundary and owns the shell context. It defaults to shell surface-3 in light mode and shell surface-2 in dark mode. SidebarInset defaults to surface-1 in both modes. Custom app shells should still pair their painted background with SurfaceProvider.

BoundaryConsumer responsibility
Cubitt sidebar app shellUse SidebarProvider; pass surfaceLevel only when the shell should use a non-default level or theme mapping
Custom app root or shellPair the painted bg-surface-* class with SurfaceProvider level={n}
App-owned inset or workspace resetAdd a nested provider only when the app paints a new root surface
Custom app-owned panelUse SurfaceProvider or Surface if the custom panel contains Cubitt components
Cubitt-owned componentsDo not add a provider; Cubitt advances or resets context internally
import {
  SidebarInset,
  SidebarProvider,
} from "@tilt-legal/cubitt-components/sidebar";

export function SidebarAppShell({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>{children}</SidebarInset>
    </SidebarProvider>
  );
}
import { SurfaceProvider } from "@tilt-legal/cubitt-components/utilities";

export function CustomAppShell({ children }: { children: React.ReactNode }) {
  return (
    <SurfaceProvider level={3}>
      <div className="min-h-screen bg-surface-3">
        <aside>App chrome</aside>

        <main className="bg-surface-1">
          <SurfaceProvider level={1}>{children}</SurfaceProvider>
        </main>
      </div>
    </SurfaceProvider>
  );
}

SurfaceProvider does not paint a background. It tells Cubitt components which surface has already been painted by the app. SidebarProvider is the exception: it paints the sidebar shell surface and provides that context for the app shell.

Cubitt-Owned Surfaces

Do not wrap normal Cubitt nesting with extra providers. If a Cubitt component owns and paints a container, it also owns the nested surface context.

For example, a Card inside a surface-1 workspace lands on surface-3 and provides surface-3 context to its children. A secondary button inside that card then lands on surface-4 without any consumer override.

import { Button } from "@tilt-legal/cubitt-components/button";
import { Card } from "@tilt-legal/cubitt-components/card";

<Card>
  <Button variant="secondary">Secondary action</Button>
</Card>;

The same rule applies to floating surfaces. Popovers, menus, sheets, selection toolbars, sidebars, and sidebar insets handle their own context when they paint a surface.

Custom App Surfaces

Use Surface when an app-owned custom container should both paint the next surface and provide that new level to its children.

import { Button } from "@tilt-legal/cubitt-components/button";
import { Surface } from "@tilt-legal/cubitt-components/utilities";

<Surface offset={2} className="rounded-xl p-4">
  <Button variant="secondary">Nested action</Button>
</Surface>;

offset={2} means parent + 2. Surface paints that computed surface-* background, adds the next border-* half-stop edge, then provides the computed surface level to nested Cubitt components.

Borders And Shadows

Surface borders and shadows both derive from surface levels, but they do different jobs.

TokenUse
border-border-1 to border-border-81px component edges and built-in divider lines. Component edges use the next stop; internal separators use two stops above the current surface.
shadow-surface-1 to shadow-surface-8Drop shadows only, matched to the floating surface level

Use borders for in-flow containers such as cards, panels, controls, and internal separators. Use shadows only when a surface floats over other content, such as popovers, menus, sheets, and toolbars.

Frosted Utilities

Frosted utilities combine a semi-transparent surface background with backdrop blur. Components can set the material surface from the active surface level; standalone usage falls back to surface-2.

<div className="material-medium rounded-lg px-4 py-2">
  <h2>Frosted glass card</h2>
  <p>Content with backdrop blur</p>
</div>
ClassOpacityBlurUse
material-ultra-thick84%30pxStrong glass separation
material-thick72%30pxNavigation bars and prominent glass cards
material-medium60%30pxDefault frosted cards and controls
material-thin48%30pxSubtle overlays
material-ultra-thin36%30pxMinimal blur

On this page