Skip to content

Instantly share code, notes, and snippets.

@mwood23
Created January 24, 2023 07:26
Show Gist options
  • Save mwood23/075f45b73d70c871155a39bca62b6902 to your computer and use it in GitHub Desktop.
Save mwood23/075f45b73d70c871155a39bca62b6902 to your computer and use it in GitHub Desktop.
Tamagui Button With Multiple Variants
import {
Stack,
styled,
GetProps,
themeable,
TamaguiElement,
VariantSpreadFunction,
} from '@tamagui/core'
import { Text } from './text'
import { forwardRef } from 'react'
export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link'
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg'
export type ButtonColorScheme =
| 'whiteAlpha'
| 'blackAlpha'
| 'primary'
| 'success'
| 'warning'
| 'error'
| 'neutral'
type VariantProps = {
variant?: ButtonVariant
size?: ButtonSize
colorScheme?: ButtonColorScheme
}
const getButtonColorScheme: VariantSpreadFunction<
GetProps<typeof Stack> & VariantProps,
ButtonColorScheme
> = (val = 'primary', { fonts, theme, props }) => {
const { variant } = props
switch (variant) {
case 'solid':
return {
backgroundColor: `$${val}.500`,
pressStyle: {
backgroundColor: `$${val}.700`,
},
hoverStyle: {
backgroundColor: `$${val}.600`,
},
focusStyle: {
backgroundColor: `$${val}.700`,
},
}
case 'link':
return {}
case 'outline':
return {
borderColor: `$${val}.500`,
pressStyle: {
backgroundColor: `$${val}.100`,
},
hoverStyle: {
backgroundColor: `$${val}.50`,
},
focusStyle: {
backgroundColor: `$${val}.100`,
},
}
case 'ghost':
return {
pressStyle: {
backgroundColor: `$${val}.100`,
},
hoverStyle: {
backgroundColor: `$${val}.50`,
},
focusStyle: {
backgroundColor: `$${val}.100`,
},
}
default:
throw new Error(variant)
}
}
const getIsDisabledColorScheme: VariantSpreadFunction<
GetProps<typeof Stack> & VariantProps,
boolean
> = (val = false, { fonts, theme, props }) => {
return val
? {
pointerEvents: 'none',
backgroundColor: `$${val}.200`,
opacity: 0.7,
}
: {}
}
const getButtonVariant: VariantSpreadFunction<
GetProps<typeof Stack> & VariantProps,
ButtonVariant
> = (val = 'solid', { fonts, theme, props }) => {
switch (val) {
case 'solid':
return {
borderColor: 'transparent',
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'nowrap',
flexDirection: 'row',
borderRadius: '$2',
}
case 'link':
return {
padding: '$0',
height: 'auto',
// borderColor: 'transparent',
// justifyContent: 'center',
// alignItems: 'center',
// flexWrap: 'nowrap',
// flexDirection: 'row',
// borderRadius: '$2',
}
case 'outline':
return {
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'nowrap',
flexDirection: 'row',
borderRadius: '$2',
}
case 'ghost':
return {
justifyContent: 'center',
alignItems: 'center',
flexWrap: 'nowrap',
flexDirection: 'row',
borderRadius: '$2',
}
default:
throw new Error(val)
}
}
const ButtonFrame = styled(Stack, {
name: 'Button',
tag: 'button',
// if we wanted this only when pressable = true, we'd need to merge variants?
cursor: 'pointer',
variants: {
size: {
xs: {
height: '$6',
paddingHorizontal: '$2',
},
sm: {
height: '$8',
paddingHorizontal: '$3',
},
md: {
height: '$10',
paddingHorizontal: '$3',
},
lg: {
height: '$12',
paddingHorizontal: '$4',
},
},
colorScheme: getButtonColorScheme,
variant: getButtonVariant,
// active: {
// true: {
// hoverStyle: {
// backgroundColor: '$background',
// },
// },
// },
isDisabled: getIsDisabledColorScheme,
} as const,
})
const getButtonTextVariant: VariantSpreadFunction<
GetProps<typeof Text> & Omit<VariantProps, 'size'>,
ButtonVariant
> = (val = 'solid', { fonts, theme, props }) => {
switch (val) {
case 'solid':
return {}
case 'link':
return {
focusStyle: {
textDecorationLine: 'underline',
},
}
case 'outline':
return {
borderColor: `$${props.colorScheme}.500`,
}
case 'ghost':
return {}
default:
throw new Error(val)
}
}
const getButtonTextColorScheme: VariantSpreadFunction<
GetProps<typeof Text> & Omit<VariantProps, 'size'>,
ButtonColorScheme
> = (val = 'primary', { fonts, theme, props }) => {
const { variant } = props
switch (variant) {
case 'solid':
return {
color: 'white',
}
case 'link':
return {
color: `$${val}.500`,
}
case 'outline':
return {
color: val === 'neutral' ? `$${val}.900` : `$${val}.500`,
}
case 'ghost':
return {
color: `$${val}.500`,
}
default:
throw new Error(variant)
}
}
const ButtonText = styled(Text, {
name: 'ButtonText',
fontWeight: '$semibold',
userSelect: 'none',
cursor: 'pointer',
// flexGrow 1 leads to inconsistent native style where text pushes to start of view
flexGrow: 0,
flexShrink: 1,
ellipse: true,
variants: {
colorScheme: getButtonTextColorScheme,
variant: getButtonTextVariant,
} as const,
})
export type ButtonProps = GetProps<typeof ButtonFrame>
const ButtonComponent = forwardRef<TamaguiElement, ButtonProps>(function Button(
{
children,
size = 'md',
colorScheme = 'primary',
variant = 'solid',
isDisabled,
...props
},
ref,
) {
return (
<ButtonFrame
size={size}
colorScheme={colorScheme}
variant={variant}
isDisabled={isDisabled}
{...props}
ref={ref}
>
<ButtonText
size={`$${size}`}
colorScheme={colorScheme}
variant={variant}
// isDisabled={isDisabled}
>
{children}
</ButtonText>
</ButtonFrame>
)
})
export const buttonStaticConfig = {
inlineProps: new Set([
// text props go here (can't really optimize them, but we never fully extract button anyway)
// may be able to remove this entirely, as the compiler / runtime have gotten better
'color',
'fontWeight',
'fontSize',
'fontFamily',
'letterSpacing',
'textAlign',
]),
}
export const Button = ButtonFrame.extractable(
themeable(ButtonComponent, ButtonFrame.staticConfig),
buttonStaticConfig,
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment