Button
Displays a button or a component that looks like a button.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ArrowUpIcon } from '#app/ui/icons.tsx'
export function ButtonDemo() {import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ArrowUpIcon } from '#app/ui/icons.tsx'
export function ButtonDemo() {
return () => (
<div
mix={css({
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: '0.5rem',
'@media (min-width: 768px)': { flexDirection: 'row' },
})}
>
<Button variant="outline">Button</Button>
<Button variant="outline" size="icon" aria-label="Submit">
<ArrowUpIcon />
</Button>
</div>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/button.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/button.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/button.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/button.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, type CSSMixinDescriptor, type Handle, type Props } from 'remix/component'
export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'
export type ButtonSize = 'default' | 'xs' | 'sm' | 'lg' | 'icon' | 'icon-xs' | 'icon-sm' | 'icon-lg'
export interface ButtonVariantOptions {
variant?: ButtonVariant
size?: ButtonSize
}
export interface ButtonProps extends Props<'button'>, ButtonVariantOptions {}
const base = css({
display: 'inline-flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
gap: '0.5rem',
borderRadius: 'calc(var(--radius) - 2px)',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
fontWeight: 500,
whiteSpace: 'nowrap',
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
outlineStyle: 'none',
boxShadow: 'var(--tw-ring-shadow), var(--tw-shadow)',
'&:focus-visible': {
borderColor: 'var(--ring)',
'--tw-ring-shadow':
'0 0 0 3px var(--tw-ring-color, color-mix(in oklab, var(--ring) 50%, transparent))',
},
'&:disabled': { pointerEvents: 'none', opacity: 0.5 },
'&[aria-invalid="true"]': {
borderColor: 'var(--destructive)',
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 20%, transparent)',
},
'.dark &[aria-invalid="true"]': {
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 40%, transparent)',
},
'& svg': { pointerEvents: 'none', flexShrink: 0 },
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
})
const variants: Record<ButtonVariant, CSSMixinDescriptor> = {
default: css({
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
'&:hover': { backgroundColor: 'color-mix(in oklab, var(--primary) 90%, transparent)' },
}),
destructive: css({
backgroundColor: 'var(--destructive)',
color: 'white',
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 20%, transparent)',
'&:hover': { backgroundColor: 'color-mix(in oklab, var(--destructive) 90%, transparent)' },
'.dark &': {
backgroundColor: 'color-mix(in oklab, var(--destructive) 60%, transparent)',
'--tw-ring-color': 'color-mix(in oklab, var(--destructive) 40%, transparent)',
},
}),
outline: css({
borderWidth: '1px',
backgroundColor: 'var(--background)',
'--tw-shadow': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
'&:hover': { backgroundColor: 'var(--accent)', color: 'var(--accent-foreground)' },
'.dark &': {
borderColor: 'var(--input)',
backgroundColor: 'color-mix(in oklab, var(--input) 30%, transparent)',
},
'.dark &:hover': { backgroundColor: 'color-mix(in oklab, var(--input) 50%, transparent)' },
'.dark &:focus-visible': { borderColor: 'var(--ring)' },
}),
secondary: css({
backgroundColor: 'var(--secondary)',
color: 'var(--secondary-foreground)',
'&:hover': { backgroundColor: 'color-mix(in oklab, var(--secondary) 80%, transparent)' },
}),
ghost: css({
'&:hover': { backgroundColor: 'var(--accent)', color: 'var(--accent-foreground)' },
'.dark &:hover': { backgroundColor: 'color-mix(in oklab, var(--accent) 50%, transparent)' },
}),
link: css({
color: 'var(--primary)',
textUnderlineOffset: '4px',
'&:hover': { textDecorationLine: 'underline' },
}),
}
const smallIcons = { '& svg:not([class*="size-"])': { width: '0.75rem', height: '0.75rem' } }
const sizes: Record<ButtonSize, CSSMixinDescriptor> = {
default: css({
height: '2.25rem',
paddingInline: '1rem',
paddingBlock: '0.5rem',
'&:has(>svg)': { paddingInline: '0.75rem' },
}),
xs: css({
height: '1.5rem',
gap: '0.25rem',
borderRadius: 'calc(var(--radius) - 2px)',
paddingInline: '0.5rem',
fontSize: '0.75rem',
lineHeight: 'calc(1 / 0.75)',
'&:has(>svg)': { paddingInline: '0.375rem' },
...smallIcons,
}),
sm: css({
height: '2rem',
gap: '0.375rem',
borderRadius: 'calc(var(--radius) - 2px)',
paddingInline: '0.75rem',
'&:has(>svg)': { paddingInline: '0.625rem' },
}),
lg: css({
height: '2.5rem',
borderRadius: 'calc(var(--radius) - 2px)',
paddingInline: '1.5rem',
'&:has(>svg)': { paddingInline: '1rem' },
}),
icon: css({ width: '2.25rem', height: '2.25rem' }),
'icon-xs': css({
width: '1.5rem',
height: '1.5rem',
borderRadius: 'calc(var(--radius) - 2px)',
...smallIcons,
}),
'icon-sm': css({ width: '2rem', height: '2rem' }),
'icon-lg': css({ width: '2.5rem', height: '2.5rem' }),
}
/**
* The `buttonVariants()` of shadcn/ui. Returns a mix you can put on any element, which is how
* you render "something that looks like a button" (shadcn's `asChild`), e.g.
* `<a href="/login" mix={buttonVariants({ variant: 'outline' })}>Login</a>`.
*/
export function buttonVariants({ variant = 'default', size = 'default' }: ButtonVariantOptions = {}) {
return [base, variants[variant], sizes[size]]
}
export function Button(handle: Handle<ButtonProps>) {
return () => {
let { variant = 'default', size = 'default', mix, ...props } = handle.props
return (
<button
data-slot="button"
data-variant={variant}
data-size={size}
{...props}
mix={[buttonVariants({ variant, size }), mix]}
/>
)
}
}Update the import paths to match your project setup.
Usage
import { Button } from '#app/ui/button.tsx'<Button variant="outline">Button</Button>Examples
Size
Use the size prop to change the size of the button.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ArrowUpRightIcon } from '#app/ui/icons.tsx'
const row = css({ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' })import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ArrowUpRightIcon } from '#app/ui/icons.tsx'
const row = css({ display: 'flex', alignItems: 'flex-start', gap: '0.5rem' })
export function ButtonSize() {
return () => (
<div
mix={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '2rem',
'@media (min-width: 640px)': { flexDirection: 'row' },
})}
>
<div mix={row}>
<Button size="sm" variant="outline">
Small
</Button>
<Button size="icon-sm" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div mix={row}>
<Button variant="outline">Default</Button>
<Button size="icon" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div mix={row}>
<Button variant="outline" size="lg">
Large
</Button>
<Button size="icon-lg" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
</div>
)
}Default
import { Button } from '#app/ui/button.tsx'
export function ButtonDefault() {
return () => <Button>Button</Button>
}import { Button } from '#app/ui/button.tsx'
export function ButtonDefault() {
return () => <Button>Button</Button>
}Outline
import { Button } from '#app/ui/button.tsx'
export function ButtonOutline() {
return () => <Button variant="outline">Outline</Button>
}import { Button } from '#app/ui/button.tsx'
export function ButtonOutline() {
return () => <Button variant="outline">Outline</Button>
}Secondary
import { Button } from '#app/ui/button.tsx'
export function ButtonSecondary() {
return () => <Button variant="secondary">Secondary</Button>
}import { Button } from '#app/ui/button.tsx'
export function ButtonSecondary() {
return () => <Button variant="secondary">Secondary</Button>
}Ghost
import { Button } from '#app/ui/button.tsx'
export function ButtonGhost() {
return () => <Button variant="ghost">Ghost</Button>
}import { Button } from '#app/ui/button.tsx'
export function ButtonGhost() {
return () => <Button variant="ghost">Ghost</Button>
}Destructive
import { Button } from '#app/ui/button.tsx'
export function ButtonDestructive() {
return () => <Button variant="destructive">Destructive</Button>
}import { Button } from '#app/ui/button.tsx'
export function ButtonDestructive() {
return () => <Button variant="destructive">Destructive</Button>
}Link
import { Button } from '#app/ui/button.tsx'
export function ButtonLink() {
return () => <Button variant="link">Link</Button>
}import { Button } from '#app/ui/button.tsx'
export function ButtonLink() {
return () => <Button variant="link">Link</Button>
}Icon
import { Button } from '#app/ui/button.tsx'
import { CircleFadingArrowUpIcon } from '#app/ui/icons.tsx'
export function ButtonIcon() {
return () => (
<Button variant="outline" size="icon" aria-label="Upgrade">import { Button } from '#app/ui/button.tsx'
import { CircleFadingArrowUpIcon } from '#app/ui/icons.tsx'
export function ButtonIcon() {
return () => (
<Button variant="outline" size="icon" aria-label="Upgrade">
<CircleFadingArrowUpIcon />
</Button>
)
}With Icon
import { Button } from '#app/ui/button.tsx'
import { GitBranchIcon } from '#app/ui/icons.tsx'
export function ButtonWithIcon() {
return () => (
<Button variant="outline" size="sm">import { Button } from '#app/ui/button.tsx'
import { GitBranchIcon } from '#app/ui/icons.tsx'
export function ButtonWithIcon() {
return () => (
<Button variant="outline" size="sm">
<GitBranchIcon /> New Branch
</Button>
)
}Rounded
Pass a css(...) mix to override styles, the equivalent of className="rounded-full".
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ArrowUpIcon } from '#app/ui/icons.tsx'
export function ButtonRounded() {import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import { ArrowUpIcon } from '#app/ui/icons.tsx'
export function ButtonRounded() {
return () => (
<div mix={css({ display: 'flex', flexDirection: 'column', gap: '2rem' })}>
<Button
variant="outline"
size="icon"
aria-label="Scroll to top"
mix={css({ borderRadius: '9999px' })}
>
<ArrowUpIcon />
</Button>
</div>
)
}Spinner
Render a <Spinner /> inside the button to show a loading state.
import { Button } from '#app/ui/button.tsx'
import { Spinner } from '#app/ui/spinner.tsx'
export function ButtonLoading() {
return () => (
<Button size="sm" variant="outline" disabled>import { Button } from '#app/ui/button.tsx'
import { Spinner } from '#app/ui/spinner.tsx'
export function ButtonLoading() {
return () => (
<Button size="sm" variant="outline" disabled>
<Spinner />
Submit
</Button>
)
}As Child
Remix components have no asChild. Apply buttonVariants() to any element to make it look like a button, here a link.
import { buttonVariants } from '#app/ui/button.tsx'
export function ButtonAsChild() {
return () => (
<a href="#login" mix={buttonVariants()}>
Loginimport { buttonVariants } from '#app/ui/button.tsx'
export function ButtonAsChild() {
return () => (
<a href="#login" mix={buttonVariants()}>
Login
</a>
)
}API Reference
Button
Renders a native <button>. Accepts every <button> attribute plus mix.
| Prop | Type | Default |
|---|---|---|
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "default" |
size | "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | "default" |
mix | MixValue | - |
buttonVariants
Returns the button styles as a mix for any element.
| Prop | Type | Default |
|---|---|---|
variant | ButtonVariant | "default" |
size | ButtonSize | "default" |