Surface

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

Surface is the starting point for panels, wells, and toolbars: a YStack with a level prop and composable visual and 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

  • Forkable recipe for app-specific Surface variants

Installation

Surface is already installed in tamagui:

import { Surface } from 'tamagui'

Surface is also a registry item you can copy into your own component layer and own; see Build your own.

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.

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

Build your own

Most apps just import Surface. Copy it into your own component layer when you want variants of your own: a card panel with different defaults than a toolbar well, your own radius, or facets tuned to your design system. The copy is a plain component: change it freely and nothing in the framework cares.

Copy it from the registry:

yarn dlx shadcn add surface

That installs components/tamagui/Surface.tsx plus its sibling components/tamagui/facets.tsx, which holds the shared facet definitions. Surface.tsx imports them through a relative ./facets import, so keep the two files together and adjust the path if your tree puts them elsewhere.

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.

API reference

Surface

A YStack with a level prop and composable facets, plus all the Tamagui standard props:

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).