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.

import { Surface } from 'tamagui'
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:

import { Surface } from 'tamagui'

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.