Base Components
Buttons that seamlessly integrate with text content.
npm install @radix-ui/react-slot
link-button.tsx
// AlignUI LinkButton v0.0.0 import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; import { PolymorphicComponentProps } from '@/utils/polymorphic'; import { recursiveCloneChildren } from '@/utils/recursive-clone-children'; import { tv, type VariantProps } from '@/utils/tv'; const LINK_BUTTON_ROOT_NAME = 'LinkButtonRoot'; const LINK_BUTTON_ICON_NAME = 'LinkButtonIcon'; export const linkButtonVariants = tv({ slots: { root: [ // base 'group inline-flex items-center justify-center whitespace-nowrap outline-none', 'transition duration-200 ease-out', 'underline decoration-transparent underline-offset-[3px]', // hover 'hover:decoration-current', // focus 'focus:outline-none focus-visible:underline', // disabled 'disabled:pointer-events-none disabled:text-text-disabled-300 disabled:no-underline', ], icon: 'shrink-0', }, variants: { variant: { gray: { root: [ // base 'text-text-sub-600', // focus 'focus-visible:text-text-strong-950', ], }, black: { root: 'text-text-strong-950', }, primary: { root: [ // base 'text-primary-base', // hover 'hover:text-primary-darker', ], }, error: { root: [ // base 'text-error-base', // hover 'hover:text-red-700', ], }, modifiable: {}, }, size: { medium: { root: 'h-5 gap-1 text-label-sm', icon: 'size-5', }, small: { root: 'h-4 gap-1 text-label-xs', icon: 'size-4', }, }, underline: { true: { root: 'decoration-current', }, }, }, defaultVariants: { variant: 'gray', size: 'medium', }, }); type LinkButtonSharedProps = VariantProps<typeof linkButtonVariants>; type LinkButtonProps = VariantProps<typeof linkButtonVariants> & React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean; }; const LinkButtonRoot = React.forwardRef<HTMLButtonElement, LinkButtonProps>( ( { asChild, children, variant, size, underline, className, ...rest }, forwardedRef, ) => { const uniqueId = React.useId(); const Component = asChild ? Slot : 'button'; const { root } = linkButtonVariants({ variant, size, underline }); const sharedProps: LinkButtonSharedProps = { variant, size, }; const extendedChildren = recursiveCloneChildren( children as React.ReactElement[], sharedProps, [LINK_BUTTON_ICON_NAME], uniqueId, asChild, ); return ( <Component ref={forwardedRef} className={root({ class: className })} {...rest} > {extendedChildren} </Component> ); }, ); LinkButtonRoot.displayName = LINK_BUTTON_ROOT_NAME; function LinkButtonIcon<T extends React.ElementType>({ className, variant, size, as, ...rest }: PolymorphicComponentProps<T, LinkButtonSharedProps>) { const Component = as || 'div'; const { icon } = linkButtonVariants({ variant, size }); return <Component className={icon({ class: className })} {...rest} />; } LinkButtonIcon.displayName = LINK_BUTTON_ICON_NAME; export { LinkButtonRoot as Root, LinkButtonIcon as Icon };
Text color inherits from the parent by default. No default color is applied, allowing for full customization.
Here's an example with custom colors specified:
This component is based on the <button> element and supports all of its props. And adds:
<button>
"gray"
"black"
"primary"
"error"
"modifiable"
"medium"
"small"
boolean
false
The LinkButton.Icon component is polymorphic, allowing you to change the underlying HTML element using the as prop.
LinkButton.Icon
as
React.ElementType
div