Surface

A copy-paste panel, well, and toolbar primitive built from facets and theme levels

Surface is a copy-paste YStack recipe with relative theme levels and composable visual or interaction facets.

Nothing is on by default. A bare <Surface /> renders no chrome and no interaction styling, and every facet is opt-in at the use site.

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Surface.tsx (registry item "surface")
// components/tamagui/facets.tsx (registry item "facets", via "surface")
// npm dependencies: @tamagui/core @tamagui/stacks
import { Surface } from "../components/tamagui/Surface";
export default () => <Surface level={2} filled outlined rounded interactive />;

Features

  • Plain YStack, level prop, and opt-in facets

  • Theme-generic facets, automatically restyled across themes and levels

  • Copy-paste fixture for app-specific Surface variants

Installation

Surface ships with tamagui:

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Surface.tsx (registry item "surface")
// components/tamagui/facets.tsx (registry item "facets", via "surface")
// npm dependencies: @tamagui/core @tamagui/stacks
import { Surface } from "../components/tamagui/Surface";

Or copy it into your own component layer from the registry, which is the intended way to own and fork it:

yarn dlx shadcn add surface

Levels

level shifts the subtree through the relative level2, level3, or level4 sub-theme. Because facets read theme generics (background, border-color, …) and the level theme re-binds those generics, a level restyles every facet beneath it with no cooperation. Read Surfaces and levels for the full theming model.

<Surface level={1} filled>Base panel</Surface>
<Surface level={2} filled>A step up</Surface>
<Surface level={3} filled>A step further</Surface>
<Surface level={4} filled>The highest default level</Surface>

level is a prop, not a variant, because a theme boundary can only be created by the theme prop or <Theme> component. <Surface level={2}> renders <Theme name="level2"> around the frame.

Facets

Facets are canonical boolean variants, each a pure function of theme generics plus standard tokens. Chrome facets own one property family each and set static styles only; the interaction facet owns pseudos only. Because family ownership keeps them from colliding, any combination composes with zero coordination.

Props

  • level

    1 | 2 | 3 | 4

    Shift the subtree through a relative level theme.

  • filled

    boolean

    Chrome: backgroundColor from background.

  • outlined

    boolean

    Chrome: 1px borderWidth with borderColor from border-color.

  • elevated

    boolean

    Chrome: a shadow read from shadow-color.

  • rounded

    boolean

    Chrome: the default component radius.

  • interactive

    boolean

    Interaction: hover, press, and focus-visible feedback read from the generics (background-hover, background-press, border-color*, and outline-color).

  • There are no preset combinations. The Material-style border-minus-fill look is just outlined without filled: a documented composition, not a separate facet.

    Copied source

    Surface is generated from a single definition. The level wrapper and the facet set are all it is:

    import { type GetProps, styled, Theme } from '@tamagui/core'
    import { YStack } from '@tamagui/stacks'
    import { forwardRef } from 'react'
    import { elevated, filled, interactive, outlined, rounded } from './facets'
    export const SurfaceFrame = styled(YStack, {
    displayName: 'Surface',
    variants: {
    filled,
    outlined,
    elevated,
    roundedFacet: rounded,
    interactive,
    } as const,
    })
    export type SurfaceProps = Omit<
    GetProps<typeof SurfaceFrame>,
    'roundedFacet' | 'rounded'
    > & {
    /** shift the subtree to a relative theme level. */
    level?: 1 | 2 | 3 | 4
    /** add the default component radius without depending on config shorthands. */
    rounded?: boolean
    }
    export const Surface = forwardRef<any, SurfaceProps>(function Surface(
    { level, rounded, ...props },
    ref
    ) {
    const frame = <SurfaceFrame ref={ref} roundedFacet={rounded} {...props} />
    if (!level || level === 1) return frame
    return <Theme name={`level${level}` as 'level2' | 'level3' | 'level4'}>{frame}</Theme>
    })

    The facets live in a sibling facets.tsx so any skin can compose the same chrome:

    export const filled = {
    true: { backgroundColor: 'background' },
    } as const
    export const outlined = {
    true: { borderWidth: 1, borderColor: 'border-color' },
    } as const
    export const elevated = {
    true: {
    shadowColor: 'shadow-color',
    shadowRadius: 8,
    shadowOffset: { width: 0, height: 2 },
    },
    } as const
    export const rounded = {
    true: { borderRadius: '4' },
    } as const
    export const interactive = {
    true: {
    backgroundColor: 'hover:background-hover press:background-press',
    borderColor: 'hover:border-color-hover press:border-color-press',
    scale: 'press:0.97',
    outlineColor: 'focus-visible:outline-color',
    outlineWidth: 'focus-visible:2px',
    outlineStyle: 'focus-visible:solid',
    },
    } as const

    Component skins like Card and ListItem do not extend Surface. They get their family resemblance by styling against the same generics, so restyling a level recolors them too. Fork the copy when you want a differently-shaped panel.

    Source

    Surface: a copied panel/well/toolbar fixture with composable chrome and interaction facets plus a relative `level` theme boundary. Nothing is on by default; every facet is opt-in. This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Surface.tsx
    // Surface — a copied panel / well / toolbar fixture, not a framework component. // It is a YStack + the composable chrome/interaction facets + a `level` prop that // shifts the subtree through the relative level themes. Nothing is on by default: a bare // <Surface /> renders no chrome and no interaction styling; every facet is opt-in // at the use site: // // <Surface level={2} filled outlined rounded interactive /> // // `level` sets a relative level theme (a theme boundary can only be created by the // prop, not by a variant), and because the facets read generics and the surface // theme re-binds those generics, facets are level-aware with zero cooperation. // Component skins do NOT extend Surface — they get their similarity by styling // against the same generics. Fork the copy for CardSurface vs ListSurface and // nothing in the framework cares. // // Generics-only: never references the color scale (colorN) directly, so it // restyles under any re-bound level. Single definition; the registry item is // generated from this file. import { type GetProps, styled, Theme } from '@tamagui/core' import { YStack } from '@tamagui/stacks' import { forwardRef } from 'react' import { elevated, filled, interactive, outlined, rounded } from './facets' export const SurfaceFrame = styled(YStack, { displayName: 'Surface', variants: { filled, outlined, elevated, roundedFacet: rounded, interactive, } as const, }) export type SurfaceProps = Omit< GetProps<typeof SurfaceFrame>, 'roundedFacet' | 'rounded' > & { /** shift the subtree to a relative theme level. */ level?: 1 | 2 | 3 | 4 /** add the default component radius without depending on config shorthands. */ rounded?: boolean } export const Surface = forwardRef<any, SurfaceProps>(function Surface( { level, rounded, ...props }, ref ) { const frame = <SurfaceFrame ref={ref} roundedFacet={rounded} {...props} /> if (!level || level === 1) return frame return <Theme name={`level${level}` as 'level2' | 'level3' | 'level4'}>{frame}</Theme> })

    Also copy

    components/tamagui/facets.tsx (registry item "facets", via "surface")

    Dependencies

    yarn add @tamagui/core @tamagui/stacks

    Expects theme tokens: background, background-hover, background-press, border-color, border-color-hover, border-color-press, shadow-color, outline-color.

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).