Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion static/app/components/core/button/styles.chonk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export function DO_NOT_USE_getChonkButtonStyles(
justifyContent: 'center',

fontWeight: p.theme.fontWeight.bold,

opacity: p.busy || p.disabled ? 0.6 : undefined,

cursor: 'pointer',
Expand Down
127 changes: 127 additions & 0 deletions static/app/components/core/layout/elevation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: Elevation Components
description: Specialized container components for creating surfaces with different elevation levels—Slabs, Wells, and Floating Sheets
source: 'sentry/components/core/layout/layer'
resources:
js: https://github.com/getsentry/sentry/blob/master/static/app/components/core/layout/layer.tsx
---

import {Container, Flex} from 'sentry/components/core/layout';
import {FloatingSheet, Slab, Well} from 'sentry/components/core/layout/elevation';
import {Text} from 'sentry/components/core/text';
import * as Storybook from 'sentry/stories';

import APIReference from '!!type-loader!sentry/components/core/layout/elevation';

export const types = {Slab: APIReference.Slab};

The Elevation module provides specialized container components that implement Sentry's [layering and elevation system](/stories/principles/layering-and-elevation). These components create visual hierarchy and help users identify interactive elements through three-dimensional depth effects.

## Slabs, Wells and Floating Sheets

Each component serves a specific purpose in the layering hierarchy:

- **Well**: Creates debossed surfaces that appear recessed into the background. Wells have increased top borders (inset box-shadow) and use a darker background to indicate lower elevation. Use wells for grouping related content in a visually contained area.

- **Slab**: Creates embossed surfaces with expressed thickness through elevated bottom borders ("chonky borders"). Slabs appear to protrude from the surface, making them ideal for interactive elements like buttons and clickable cards. The border thickness corresponds to the element's depth.

- **FloatingSheet**: Creates elevated surfaces that hover above the main content layer. Unlike slabs and wells which are attached to the content surface, floating sheets cast shadows on the content beneath them to indicate higher elevation. They don't use chonky borders, making them suitable for overlays, modals, and tooltips.

<Storybook.Demo>
<Storybook.SideBySide>
<Well padding="md" width="100px" height="100px">
Well
</Well>
<Slab padding="md" width="100px" height="100px">
Slab
</Slab>
<FloatingSheet padding="md" width="100px" height="100px">
Floating Sheet
</FloatingSheet>
</Storybook.SideBySide>
</Storybook.Demo>
```jsx
<Well>Well</Well>
<Slab>Slab</Slab>
<FloatingSheet>Floating Sheet</FloatingSheet>
```

### Elevation

Control the elevation and thickness of the slab or floating sheets using the `size` prop. Available sizes are `xs`, `sm`, and `md` (default). Smaller sizes create a more subtle elevated effect.

<Storybook.Demo>
<Storybook.SideBySide>
<Flex
background="primary"
padding="xl"
radius="md"
direction="row"
gap="xl"
border="primary"
>
<Flex direction="column" gap="xl">
<strong>Slab</strong>
<Slab padding="md" size="xs" width="100px">
<Text align="center">xs</Text>
</Slab>
<Slab padding="md" size="sm" width="100px">
<Text align="center">sm</Text>
</Slab>
<Slab padding="md" size="md" width="100px">
<Text align="center">md</Text>
</Slab>
</Flex>
<Flex direction="column" gap="xl">
<strong>Floating Sheet</strong>
<FloatingSheet padding="md" size="xs" width="100px">
<Text align="center">xs</Text>
</FloatingSheet>
<FloatingSheet padding="md" size="sm" width="100px">
<Text align="center">sm</Text>
</FloatingSheet>
<FloatingSheet padding="md" size="md" width="100px">
<Text align="center">md</Text>
</FloatingSheet>
</Flex>
</Flex>
</Storybook.SideBySide>
</Storybook.Demo>
```jsx
<Slab size="xs">Extra small elevation</Slab>
<Slab size="sm">Small elevation</Slab>
<Slab size="md">Default elevation</Slab>

<FloatingSheet size="xs">Extra small elevation</FloatingSheet>
<FloatingSheet size="sm">Small elevation</FloatingSheet>
<FloatingSheet size="md">Default elevation</FloatingSheet>
```

### Slab Variants

Slabs support color variants for different semantic purposes. The `variant` prop changes both the surface and background colors to indicate accent, warning, or danger states.

<Storybook.Demo>
<Storybook.SideBySide>
<Flex direction="column" gap="xl">
<Slab padding="md" width="120px">
<Text align="center">Default</Text>
</Slab>
<Slab padding="md" width="120px" variant="accent">
<Text align="center">Accent</Text>
</Slab>
<Slab padding="md" width="120px" variant="warning">
<Text align="center">Warning</Text>
</Slab>
<Slab padding="md" width="120px" variant="danger">
<Text align="center">Danger</Text>
</Slab>
</Flex>
</Storybook.SideBySide>
</Storybook.Demo>
```jsx
<Slab>Default</Slab>
<Slab variant="accent">Accent</Slab>
<Slab variant="warning">Warning</Slab>
<Slab variant="danger">Danger</Slab>
```
230 changes: 230 additions & 0 deletions static/app/components/core/layout/elevation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import {type DO_NOT_USE_ChonkTheme} from '@emotion/react';
import styled from '@emotion/styled';

import {chonkFor, debossedBackground} from 'sentry/components/core/chonk';
import {
Container,
type ContainerElement,
type ContainerProps,
} from 'sentry/components/core/layout/container';
import {rc, type Responsive} from 'sentry/components/core/layout/styles';
import {isChonkTheme} from 'sentry/utils/theme/withChonk';

const slabElevation = {
md: '2px',
sm: '2px',
xs: '1px',
} as const;

const slabHoverElevation = '1px';
interface SlabLayoutProps {
size?: NonNullable<Responsive<'sm' | 'md' | 'xs'>>;
variant?: 'accent' | 'warning' | 'danger';
}

type SlabProps<T extends ContainerElement = 'div'> = ContainerProps<T> & SlabLayoutProps;

export function Slab<T extends ContainerElement = 'div'>(props: SlabProps<T>) {
const {variant, border, radius = 'md', size = 'md', ...rest} = props;

return (
<SlabContainer
size={size}
variant={variant}
radius={radius}
// Forward layout props to the container so that the slab
// can be used as a flex or grid item
flex={props.flex}
flexBasis={props.flexBasis}
flexGrow={props.flexGrow}
flexShrink={props.flexShrink}
justifySelf={props.justifySelf}
alignSelf={props.alignSelf}
// Forward grid props to the container so that the slab
// can be used as a grid item
area={props.area}
>
<Container
border={variant ?? 'primary'}
radius={radius}
background="primary"
{...rest}
/>
</SlabContainer>
);
}

function getSlabContainerTheme(
variant: SlabLayoutProps['variant'],
theme: DO_NOT_USE_ChonkTheme
) {
switch (variant) {
case 'accent':
return {
surface: theme.colors.blue400,
background: chonkFor(theme, theme.colors.blue400),
};
case 'warning':
return {
surface: theme.colors.chonk.yellow400,
background: chonkFor(theme, theme.colors.chonk.yellow400),
};
case 'danger':
return {
surface: theme.colors.chonk.red400,
background: chonkFor(theme, theme.colors.chonk.red400),
};
default:
return {
surface: theme.colors.surface500,
background: theme.colors.surface100,
};
}
}

const SlabContainer = styled(Container)<{
size: SlabLayoutProps['size'];
variant: SlabLayoutProps['variant'];
}>`
background-color: ${p =>
isChonkTheme(p.theme)
? getSlabContainerTheme(p.variant, p.theme).background
: p.theme.tokens.background.primary};

display: ${p => (isChonkTheme(p.theme) ? undefined : 'contents')};

> * {
${p =>
rc(
'transform',
p.size,
p.theme,
(value, _breakpoint, _theme) => `translateY(-${slabElevation[value]})`
)};
transition: transform ${p => p.theme.motion.snap.fast};
}

&:hover {
> * {
${p =>
rc(
'transform',
p.size,
p.theme,
(value, _breakpoint, _theme) =>
`translateY(calc(-${slabElevation[value]} - ${slabHoverElevation}))`
)};
}
}

&:active {
> * {
transform: translateY(0);
}
}

&:has(> *[aria-disabled='true']),
&[aria-disabled='true'] {
> * {
transform: translateY(0);
}
}
`;

interface WellLayoutProps {}
type WellProps<T extends ContainerElement = 'div'> = ContainerProps<T> & WellLayoutProps;

export const Well = styled(<T extends ContainerElement = 'div'>(props: WellProps<T>) => {
const {radius = 'md', ...rest} = props;

return <Container border="primary" radius={radius} background="primary" {...rest} />;
})`
${p => {
if (isChonkTheme(p.theme)) {
return {
boxShadow: `0px 2px 0px 0px ${p.theme.tokens.border.primary} inset`,
...debossedBackground(p.theme),
};
}
return {
backgroundColor: p.theme.tokens.background.primary,
};
}}
`;

interface FloatingSheetLayoutProps {
size?: Responsive<'sm' | 'md' | 'xs'>;
}
type FloatingSheetProps<T extends ContainerElement = 'div'> = ContainerProps<T> &
FloatingSheetLayoutProps;

export const FloatingSheet = styled(
<T extends ContainerElement = 'div'>(props: FloatingSheetProps<T>) => {
const {
size = 'md',
radius = 'md',
background = 'primary',
border = 'primary',
...rest
} = props;

return (
<FloatingSheetContainer size={size} radius={radius}>
<Container
background={background}
border={border}
radius={radius}
position="relative"
{...rest}
/>
</FloatingSheetContainer>
);
}
)<FloatingSheetProps>``;

const floatingSheetElevation = {
md: '6px',
sm: '5px',
xs: '4px',
} as const;

const FloatingSheetContainer = styled(Container)<{
size: FloatingSheetLayoutProps['size'];
}>`
position: relative;
${p =>
rc(
'transform',
p.size,
p.theme,
(value, _breakpoint, _theme) => `translateY(-${floatingSheetElevation[value]})`
)}

> * {
position: relative;
z-index: 1;
}

&:after {
content: '';
position: absolute;
top: 0;
left: ${p => p.theme.borderRadius};
right: ${p => p.theme.borderRadius};
bottom: 0;
background-color: ${p =>
isChonkTheme(p.theme)
? p.theme.colors.surface100
: p.theme.tokens.background.secondary};
border-radius: ${p =>
isChonkTheme(p.theme) ? p.theme.radius.md : p.theme.borderRadius};
z-index: 0;
${p =>
rc(
'transform',
p.size,
p.theme,
(value, _breakpoint, _theme) => `translateY(${floatingSheetElevation[value]})`
)};
}
`;
2 changes: 1 addition & 1 deletion static/app/components/core/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const Link = styled(({disabled, to, ...props}: LinkProps) => {
const {state: appState} = useFrontendVersion();

if (disabled || !location) {
return <Anchor {...props} />;
return <Anchor {...props} aria-disabled={disabled} />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ Apply elevation in a manner consistent with the model outlined below.

**Slabs and wells** are sheets with an expressed thickness dimension, i.e. those with increased bottom borders (embossed slabs) or top borders (debossed wells). The border thickness corresponds to the element's thickness/depth. Larger buttons, for example, are thicker and therefore have more Chonky borders. Use slabs and wells for interactive elements to help make them stand out.

See the [Slab](/stories/layout/layer#slabs-wells-and-floating-sheets) and [Well](/stories/layout/layer#slabs-wells-and-floating-sheets) components.

### Floating Elements

**Floating elements** are sheets that hover above the main content (as opposed to slabs and wells, which are still attached to the content) and therefore cast a shadow on the content underneath. This group includes tooltips, overlays, modals, and toasts. They do not need to have a thickness dimension — no Chonky border.

See the [FloatingSheet](/stories/layout/layer#slabs-wells-and-floating-sheets) component.
Loading