Pressable

A pressable is a primitive for building custom buttons.

Installation

Package installation information
Installyarn add @atlaskit/primitives
SourceBitbucket.org, (opens new window)
npm@atlaskit/primitives, (opens new window)
Bundleunpkg.com, (opens new window)

Pressable is a primitive for building custom buttons with Atlassian Design System styling and built-in event tracking. It renders a <button> element. Use pressable when existing buttons can't be customized to fit your needs.

Default

Pressable is unstyled by default, aside from basic focus styles.

import React, { useCallback } from 'react'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; export default function Default(): React.JSX.Element { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return <Pressable onClick={handleClick}>Pressable</Pressable>; }

Basic styling

Pressable can be styled further using the design system styling API, cssMap.

Make sure styling indicates the interaction state using :hover and :active pseudo-classes.

Pressable does not include button motion by default. For a custom button that cannot use the Button component, add button motion tokens to the background-color transition:

const styles = cssMap({
    root: {
        backgroundColor: token('color.background.neutral'),
        transition: token('motion.button.hovered'),
        '&:hover': {
            backgroundColor: token('color.background.neutral.hovered'),
        },
        '&:active': {
            backgroundColor: token('color.background.neutral.pressed'),
            transition: token('motion.button.pressed'),
        },
    },
});

Use button motion only for Pressable elements that are semantically buttons. Keep Pressable cards, list items, navigation items, links, and form controls motion-neutral or use the motion tokens for their semantic component.

For a custom list item built with Pressable, use list-item motion tokens for the interactive state transitions:

import { cssMap } from '@atlaskit/css';
import { Pressable } from '@atlaskit/primitives/compiled';
import { token } from '@atlaskit/tokens';

const listItemStyles = cssMap({
    root: {
        transition: token('motion.listitem.hovered'),
        '&:hover': {
            transition: token('motion.listitem.hovered'),
        },
        '&:active': {
            transition: token('motion.listitem.pressed'),
        },
        '&[data-selected]': {
            transition: token('motion.listitem.selected'),
        },
        '&[data-selected]:hover': {
            transition: token('motion.listitem.hovered'),
        },
        '&[data-selected]:active': {
            transition: token('motion.listitem.pressed'),
        },
        '&:focus-visible': {
            transition: 'none',
        },
    },
});

export function CustomListItem({ isSelected, children, onClick }) {
    return (
        <Pressable
            xcss={listItemStyles.root}
            data-selected={isSelected ? 'true' : undefined}
            onClick={onClick}
        >
            {children}
        </Pressable>
    );
}
import { type JSX, useCallback } from 'react'; import { cssMap, jsx } from '@atlaskit/css'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; import { token } from '@atlaskit/tokens'; const styles = cssMap({ pressable: { color: token('color.text.subtle'), fontWeight: token('font.weight.medium'), backgroundColor: token('color.background.neutral.subtle'), transition: token('motion.button.hovered'), paddingBlockStart: token('space.0'), paddingInlineEnd: token('space.0'), paddingBlockEnd: token('space.0'), paddingInlineStart: token('space.0'), '&:hover': { textDecoration: 'underline', backgroundColor: token('color.background.neutral.subtle.hovered'), }, '&:active': { color: token('color.link.pressed'), backgroundColor: token('color.background.neutral.subtle.pressed'), transition: token('motion.button.pressed'), }, }, }); export default function Basic(): JSX.Element { const handleClick = useCallback(() => { console.log('Clicked'); }, []); return ( <Pressable onClick={handleClick} xcss={styles.pressable}> Edit comment </Pressable> ); }

Advanced styling

Use a combination of cssMap and other primitives for more complex designs.

You're following 5 active projects, here's the breakdown.
import type { JSX } from 'react'; import { cssMap, jsx } from '@atlaskit/css'; import { Box } from '@atlaskit/primitives/compiled/box'; import type { TextColor } from '@atlaskit/primitives/compiled/components/types'; import { Flex } from '@atlaskit/primitives/compiled/flex'; import { Grid } from '@atlaskit/primitives/compiled/grid'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; import { Stack } from '@atlaskit/primitives/compiled/stack'; import { Text } from '@atlaskit/primitives/compiled/text'; import { token } from '@atlaskit/tokens'; const styles = cssMap({ pressable: { paddingBlockStart: token('space.150'), paddingInlineEnd: token('space.150'), paddingBlockEnd: token('space.150'), paddingInlineStart: token('space.150'), borderRadius: token('radius.small'), borderColor: token('color.border'), borderWidth: token('border.width'), borderStyle: 'solid', color: token('color.text'), backgroundColor: token('color.background.neutral.subtle'), '&:hover': { backgroundColor: token('color.background.neutral.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.neutral.subtle.pressed'), }, }, value: { font: token('font.heading.xlarge'), }, grid: { '@media (min-width: 48rem)': { gridTemplateColumns: '1fr 1fr', }, '@media (min-width: 64rem)': { gridTemplateColumns: '1fr 1fr 1fr', }, gridTemplateColumns: '1fr', rowGap: token('space.100'), columnGap: token('space.100'), }, }); const ProjectStatus = ({ value, title, subtitle, color, }: { value: number; title: string; subtitle: string; color: TextColor; }) => { return ( <Pressable xcss={styles.pressable}> <Flex as="span" gap="space.150" alignItems="center"> <Text color={color}> <Box as="span" xcss={styles.value}> {value} </Box> </Text> <Stack as="span" space="space.0" alignInline="start"> <Text weight="semibold">{title}</Text> <Text size="small" color="color.text.subtlest"> {subtitle} </Text> </Stack> </Flex> </Pressable> ); }; export default function Styled(): JSX.Element { return ( <Stack space="space.150"> <Text weight="bold" size="large"> You're following 5 active projects, here's the breakdown. </Text> <Grid xcss={styles.grid}> <ProjectStatus value={2} title="On track" subtitle="-1 from last week" color="color.text.success" /> <ProjectStatus value={1} title="At risk" subtitle="+1 from last week" color="color.text.warning" /> <ProjectStatus value={0} title="Off track" subtitle="No change" color="color.text.danger" /> <ProjectStatus value={2} title="No update" subtitle="+2 from last week" color="color.text.discovery" /> <ProjectStatus value={0} title="Cancelled" subtitle="No change" color="color.text.subtle" /> <ProjectStatus value={1} title="Completed" subtitle="+1 from last week" color="color.text.information" /> </Grid> </Stack> ); }

Disabled

You can disable pressable buttons with the isDisabled prop. Disabled styles should be applied and defined conditionally using cssMap.

Disabled buttons can cause accessibility issues (disabled elements are not in the tab order) so wherever possible, avoid using isDisabled. Instead, use validation or other techniques to show users how to proceed.

import { type JSX, useCallback, useState } from 'react'; import { cssMap, cx, jsx } from '@atlaskit/css'; import { Inline } from '@atlaskit/primitives/compiled/inline'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; import { Stack } from '@atlaskit/primitives/compiled/stack'; import Toggle from '@atlaskit/toggle'; import { token } from '@atlaskit/tokens'; const styles = cssMap({ pressable: { fontWeight: token('font.weight.medium'), backgroundColor: token('color.background.neutral.subtle'), paddingBlockStart: token('space.0'), paddingInlineEnd: token('space.0'), paddingBlockEnd: token('space.0'), paddingInlineStart: token('space.0'), }, enabled: { color: token('color.text.subtle'), '&:hover': { textDecoration: 'underline', backgroundColor: token('color.background.neutral.subtle.hovered'), }, '&:active': { color: token('color.link.pressed'), backgroundColor: token('color.background.neutral.subtle.pressed'), }, }, disabled: { color: token('color.text.disabled'), }, }); export default function Disabled(): JSX.Element { const handleClick = useCallback(() => { console.log('Clicked'); }, []); const [isDisabled, setIsDisabled] = useState(true); const toggleDisabled = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { setIsDisabled(event.currentTarget.checked); }, []); return ( <Stack space="space.200" alignInline="start"> <Inline alignBlock="center" space="space.100"> <Toggle isChecked={isDisabled} id="is-disabled" onChange={toggleDisabled} /> <label htmlFor="is-disabled">Disabled</label> </Inline> <Pressable isDisabled={isDisabled} onClick={handleClick} xcss={cx(styles.pressable, isDisabled ? styles.disabled : styles.enabled)} > Edit comment </Pressable> </Stack> ); }

Buttons without visible labels

For buttons without visible labels such as icon buttons, make an accessible label available using the visually hidden component. This renders hidden text inside the button for assistive technologies, which is preferable to an aria-label attribute because not all screen readers translate these between languages.

Also, consider providing a tooltip to help sighted users understand the button's purpose.

import type { JSX } from 'react'; import ButtonGroup from '@atlaskit/button/button-group'; import { cssMap, jsx } from '@atlaskit/css'; import EmojiAddIcon from '@atlaskit/icon/core/emoji-add'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; import { Text } from '@atlaskit/primitives/compiled/text'; import { token } from '@atlaskit/tokens'; import Tooltip from '@atlaskit/tooltip/Tooltip'; import VisuallyHidden from '@atlaskit/visually-hidden/visually-hidden'; import { ReactionsList } from '../../utils/reactions'; const styles = cssMap({ pressable: { backgroundColor: token('color.background.neutral.subtle'), borderWidth: token('border.width'), borderStyle: 'solid', borderColor: token('color.border'), borderRadius: token('radius.large'), paddingInline: token('space.100'), height: '27px', display: 'flex', alignItems: 'center', '&:hover': { backgroundColor: token('color.background.neutral.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.neutral.subtle.pressed'), }, }, }); type ReactionButtonProps = { emoji?: string; name?: string; reactions?: number; }; const ReactionButton = ({ emoji, name, reactions }: ReactionButtonProps) => { return ( <Tooltip content={ name && reactions ? ( <p> <strong>{name}</strong> <ReactionsList reactions={reactions} /> </p> ) : ( 'Add a reaction' ) } > <Pressable xcss={styles.pressable}> {emoji ? ( <Text size="small" color="color.text.subtle"> {emoji} {reactions} </Text> ) : ( <EmojiAddIcon color={token('color.icon')} label="" /> )} <VisuallyHidden>Add a {name && `${name} `}reaction</VisuallyHidden> </Pressable> </Tooltip> ); }; export default function IconButtons(): JSX.Element { return ( <ButtonGroup label="Reactions"> <ReactionButton emoji="👏" name="Clap" reactions={26} /> <ReactionButton emoji="❤️" name="Heart" reactions={4} /> <ReactionButton emoji="👍" name="Thumbs up" reactions={17} /> <ReactionButton /> </ButtonGroup> ); }

HTML attributes

Pressable passes all valid HTML attributes to the underlying <button> element. The type attribute defaults to button to prevent unintentionally submitting forms.

import React from 'react'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; export default function HtmlAttributes(): React.JSX.Element { return <Pressable type="submit">Submit form</Pressable>; }

Event tracking

Pressable has utilities to make tracking events easier. Events won't be captured unless listeners are set up to handle them.

Track events for any analytics provider

Pressable comes with built-in Atlaskit analytics support using the Analytics next package, (opens new window), and fires events for available listeners. Currently this is only available for onClick.

Events always fire on the atlaskit channel. To fire events on other channels as well, use the provided analyticsEvent in onClick. To configure event data, use componentName (defaults to 'Pressable') and use analyticsContext to pass other metadata.

See the event data in the console.

import React, { useCallback } from 'react'; import AnalyticsListener from '@atlaskit/analytics-next/AnalyticsListener'; import type UIAnalyticsEvent from '@atlaskit/analytics-next/UIAnalyticsEvent'; import ButtonGroup from '@atlaskit/button/button-group'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; export default function Analytics(): React.JSX.Element { const handleEvent = useCallback((event: UIAnalyticsEvent, channel?: string) => { console.log(`Channel: '${channel}'`, event); }, []); return ( <AnalyticsListener channel="*" onEvent={handleEvent}> <ButtonGroup label="Pressable buttons with analytics"> <Pressable>Default</Pressable> <Pressable onClick={(_, analyticsEvent) => { analyticsEvent.fire('my-channel'); }} > Fires on "my-channel" </Pressable> <Pressable componentName="MyButton" analyticsContext={{ color: 'blue', someId: 937458, }} > Customized event data </Pressable> </ButtonGroup> </AnalyticsListener> ); }

Track events for Atlassian internal services

GASv3 analytics

The Atlassian analytics bridge makes Atlaskit analytics events compatible with GASv3 (Global Analytics Service). This can also inject an actionSubjectId to the event if required.

See the event data in the console.

import React, { useCallback } from 'react'; import AnalyticsListener from '@atlaskit/analytics-next/AnalyticsListener'; import type UIAnalyticsEvent from '@atlaskit/analytics-next/UIAnalyticsEvent'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; import { ANALYTICS_BRIDGE_CHANNEL, extractAWCDataFromEvent, fireUIAnalytics, } from '@atlassian/analytics-bridge'; export default function AnalyticsGASv3(): React.JSX.Element { const handleEvent = useCallback((event: UIAnalyticsEvent, channel?: string) => { console.log(`Channel: '${channel}'`, extractAWCDataFromEvent(event)); }, []); const handleClick = useCallback( (_: React.MouseEvent<HTMLButtonElement, MouseEvent>, analyticsEvent: UIAnalyticsEvent) => { fireUIAnalytics(analyticsEvent, 'theActionSubjectId'); }, [], ); return ( <AnalyticsListener channel={ANALYTICS_BRIDGE_CHANNEL} onEvent={handleEvent}> <Pressable onClick={handleClick} analyticsContext={{ attributes: { color: 'blue', someId: 937458, }, }} > Fire GASv3 compatible event </Pressable> </AnalyticsListener> ); }

React UFO press interactions

By default, pressable fires React UFO (Unified Frontend Observability) press interactions, (opens new window) for available listeners. This helps Atlassian measure performance and reliability. You can provide more detail using the interactionName prop.

Change epic color

import { type JSX, useState } from 'react'; import { cssMap, cx, jsx } from '@atlaskit/css'; import __noop from '@atlaskit/ds-lib/noop'; import { FlagsProvider } from '@atlaskit/flag/flags-provider'; import { useFlags } from '@atlaskit/flag/use-flags'; import Heading from '@atlaskit/heading/heading'; import CheckMarkIcon from '@atlaskit/icon/core/check-mark'; import InformationIcon from '@atlaskit/icon/core/status-information'; import InteractionContext from '@atlaskit/interaction-context'; import ZoomIn from '@atlaskit/motion/zoom-in'; import { Box } from '@atlaskit/primitives/compiled/box'; import { Flex } from '@atlaskit/primitives/compiled/flex'; import { Inline } from '@atlaskit/primitives/compiled/inline'; import { Pressable } from '@atlaskit/primitives/compiled/pressable'; import { Stack } from '@atlaskit/primitives/compiled/stack'; import { token } from '@atlaskit/tokens'; import Tooltip from '@atlaskit/tooltip/Tooltip'; import VisuallyHidden from '@atlaskit/visually-hidden/visually-hidden'; const iconSpacingStyles = cssMap({ space050: { paddingBlock: token('space.050'), paddingInline: token('space.050'), }, }); const styles = cssMap({ base: { borderWidth: token('border.width'), borderStyle: 'solid', borderColor: token('color.border'), borderRadius: token('radius.small'), height: '44px', width: '44px', display: 'flex', alignItems: 'center', justifyContent: 'center', }, }); const colorStyles = cssMap({ Red: { backgroundColor: token('color.background.accent.red.subtle'), '&:hover': { backgroundColor: token('color.background.accent.red.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.red.subtle.pressed'), }, }, Orange: { backgroundColor: token('color.background.accent.orange.subtle'), '&:hover': { backgroundColor: token('color.background.accent.orange.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.orange.subtle.pressed'), }, }, Yellow: { backgroundColor: token('color.background.accent.yellow.subtle'), '&:hover': { backgroundColor: token('color.background.accent.yellow.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.yellow.subtle.pressed'), }, }, Lime: { backgroundColor: token('color.background.accent.lime.subtle'), '&:hover': { backgroundColor: token('color.background.accent.lime.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.lime.subtle.pressed'), }, }, Green: { backgroundColor: token('color.background.accent.green.subtle'), '&:hover': { backgroundColor: token('color.background.accent.green.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.green.subtle.pressed'), }, }, Teal: { backgroundColor: token('color.background.accent.teal.subtle'), '&:hover': { backgroundColor: token('color.background.accent.teal.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.teal.subtle.pressed'), }, }, Blue: { backgroundColor: token('color.background.accent.blue.subtle'), '&:hover': { backgroundColor: token('color.background.accent.blue.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.blue.subtle.pressed'), }, }, Purple: { backgroundColor: token('color.background.accent.purple.subtle'), '&:hover': { backgroundColor: token('color.background.accent.purple.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.purple.subtle.pressed'), }, }, Magenta: { backgroundColor: token('color.background.accent.magenta.subtle'), '&:hover': { backgroundColor: token('color.background.accent.magenta.subtle.hovered'), }, '&:active': { backgroundColor: token('color.background.accent.magenta.subtle.pressed'), }, }, }); type ColorButtonProps = { color: keyof typeof colorStyles; isSelected?: boolean; onClick?(): void; }; const ColorButton = ({ color, isSelected, onClick }: ColorButtonProps) => { return ( <Tooltip content={color}> <Pressable interactionName={`color-${color.toLowerCase()}`} xcss={cx(styles.base, colorStyles[color])} aria-pressed={isSelected} onClick={onClick} > {isSelected && ( <ZoomIn> {(props) => ( <div {...props}> <CheckMarkIcon label="" color={token('color.icon.inverse')} /> </div> )} </ZoomIn> )} <VisuallyHidden>{color}</VisuallyHidden> </Pressable> </Tooltip> ); }; const ColorPaletteButtons = () => { const [selectedColor, setSelectedColor] = useState<keyof typeof colorStyles | null>('Red'); const { showFlag } = useFlags(); return ( <InteractionContext.Provider value={{ hold: __noop, tracePress: (name) => { showFlag({ title: `Traced a press!`, description: name, icon: ( <Flex xcss={iconSpacingStyles.space050}> <InformationIcon label="Info" color={token('color.icon.information')} /> </Flex> ), isAutoDismiss: true, }); }, }} > <Stack space="space.150" alignInline="start"> <Heading size="small" id="epic-heading"> Change epic color </Heading> <Box role="group" aria-labelledby="epic-heading"> <Inline space="space.100"> {Object.keys(colorStyles).map((color) => { const keyColor = color as keyof typeof colorStyles; return ( <ColorButton key={keyColor} color={keyColor} isSelected={selectedColor === keyColor} onClick={() => setSelectedColor(keyColor)} /> ); })} </Inline> </Box> </Stack> </InteractionContext.Provider> ); }; export default function PressTracing(): JSX.Element { return ( <FlagsProvider> <ColorPaletteButtons /> </FlagsProvider> ); }
Was this page helpful?
We use this feedback to improve our documentation.
© 2026 AtlassianTrademark, (opens new window)Privacy, (opens new window)License