From b8fcf3a21681201551bf9fc24aa0074e027d356f Mon Sep 17 00:00:00 2001 From: Mohamed Anwar Date: Mon, 10 Aug 2026 03:40:59 +0300 Subject: [PATCH 1/2] feat(button): add ghost variant --- .changeset/bright-buttons-ghost.md | 5 ++ apps/storybook/src/Button.stories.tsx | 8 +++ apps/web/content/docs/components/button.mdx | 6 ++ apps/web/examples/button/button.ghost.tsx | 63 +++++++++++++++++++ apps/web/examples/button/index.ts | 1 + .../components/Button/Button.ghost.test.tsx | 39 ++++++++++++ packages/ui/src/components/Button/Button.tsx | 5 +- packages/ui/src/components/Button/theme.ts | 28 ++++++++- 8 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 .changeset/bright-buttons-ghost.md create mode 100644 apps/web/examples/button/button.ghost.tsx create mode 100644 packages/ui/src/components/Button/Button.ghost.test.tsx diff --git a/.changeset/bright-buttons-ghost.md b/.changeset/bright-buttons-ghost.md new file mode 100644 index 0000000000..080ed40cbb --- /dev/null +++ b/.changeset/bright-buttons-ghost.md @@ -0,0 +1,5 @@ +--- +"flowbite-react": minor +--- + +Add a `ghost` button variant that works with the existing button colors. diff --git a/apps/storybook/src/Button.stories.tsx b/apps/storybook/src/Button.stories.tsx index 7287b0169a..e56396ca70 100644 --- a/apps/storybook/src/Button.stories.tsx +++ b/apps/storybook/src/Button.stories.tsx @@ -27,3 +27,11 @@ DefaultButton.storyName = "Default"; DefaultButton.args = { children: "Button", }; + +export const GhostButton = Template.bind({}); +GhostButton.storyName = "Ghost"; +GhostButton.args = { + children: "Ghost button", + color: "green", + ghost: true, +}; diff --git a/apps/web/content/docs/components/button.mdx b/apps/web/content/docs/components/button.mdx index 5fb5121896..339df8743b 100644 --- a/apps/web/content/docs/components/button.mdx +++ b/apps/web/content/docs/components/button.mdx @@ -43,6 +43,12 @@ Use the `outline` property to create an outline button with transparent backgrou +## Ghost buttons + +Use the `ghost` property to create a button with a transparent background and no visible border. Combine it with the `color` property to keep the same semantic color while showing a subtle background on hover. + + + ## Button sizes You can update the size of the button by adding the `size` property to the ` + + + + + + + ); +} +`; + +export function Component() { + return ( +
+ + + + + + +
+ ); +} + +export const ghost: CodeData = { + type: "single", + code: { + fileName: "index", + language: "tsx", + code, + }, + githubSlug: "button/button.ghost.tsx", + component: , +}; diff --git a/apps/web/examples/button/index.ts b/apps/web/examples/button/index.ts index 21be73f760..70ad52a9b0 100644 --- a/apps/web/examples/button/index.ts +++ b/apps/web/examples/button/index.ts @@ -1,4 +1,5 @@ export { disabled } from "./button.disabled"; +export { ghost } from "./button.ghost"; export { gradientDuo } from "./button.gradientDuo"; export { gradientMono } from "./button.gradientMono"; export { iconOnly } from "./button.iconOnly"; diff --git a/packages/ui/src/components/Button/Button.ghost.test.tsx b/packages/ui/src/components/Button/Button.ghost.test.tsx new file mode 100644 index 0000000000..a6123b9ee3 --- /dev/null +++ b/packages/ui/src/components/Button/Button.ghost.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { ThemeProvider } from "../../theme/provider"; +import type { CustomFlowbiteTheme } from "../../types"; +import { Button } from "./Button"; + +describe("Components / Button / ghost", () => { + it("should use ghost color classes instead of solid color classes", () => { + const theme: CustomFlowbiteTheme = { + button: { + color: { + red: "bg-red-700", + }, + ghostColor: { + red: "text-red-700 hover:bg-red-100", + }, + }, + }; + + render( + + + , + ); + + const ghostButton = screen.getByRole("button", { name: "Delete" }); + + expect(ghostButton).toHaveClass("text-red-700", "hover:bg-red-100"); + expect(ghostButton).not.toHaveClass("bg-red-700"); + }); + + it("should not forward the ghost prop to the DOM", () => { + render(); + + expect(screen.getByRole("button", { name: "Ghost" })).not.toHaveAttribute("ghost"); + }); +}); diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index aa296893c0..0d4d0b1be7 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -28,6 +28,7 @@ export interface ButtonTheme { size: ButtonSizes; // colors color: ButtonColors; + ghostColor: ButtonColors; outlineColor: ButtonOutlineColors; } @@ -51,6 +52,7 @@ export type ButtonProps = PolymorphicComponent href?: string; color?: DynamicStringEnumKeysOf; fullSized?: boolean; + ghost?: boolean; outline?: boolean; pill?: boolean; size?: DynamicStringEnumKeysOf; @@ -76,6 +78,7 @@ export const Button = forwardRef((props: Butto color = "default", disabled, fullSized, + ghost, outline: _outline, pill: _pill, size = "md", @@ -97,7 +100,7 @@ export const Button = forwardRef((props: Butto pill && theme.pill, disabled && theme.disabled, fullSized && theme.fullSized, - outline ? theme.outlineColor[color] : theme.color[color], + ghost ? theme.ghostColor[color] : outline ? theme.outlineColor[color] : theme.color[color], buttonGroup && theme.grouped, className, )} diff --git a/packages/ui/src/components/Button/theme.ts b/packages/ui/src/components/Button/theme.ts index 1eb5915b53..dc44a72e12 100644 --- a/packages/ui/src/components/Button/theme.ts +++ b/packages/ui/src/components/Button/theme.ts @@ -34,11 +34,35 @@ export const buttonTheme = createTheme({ pink: "bg-pink-700 text-white hover:bg-pink-800 focus:ring-pink-300 dark:bg-pink-600 dark:hover:bg-pink-700 dark:focus:ring-pink-800", purple: "bg-purple-700 text-white hover:bg-purple-800 focus:ring-purple-300 dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-800", - red: "bg-red-700 text-white hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800", + red: "bg-red-700 text-white hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900", teal: "bg-teal-700 text-white hover:bg-teal-800 focus:ring-teal-300 dark:bg-teal-600 dark:hover:bg-teal-700 dark:focus:ring-teal-800", yellow: "bg-yellow-400 text-white hover:bg-yellow-500 focus:ring-yellow-300 dark:bg-yellow-600 dark:hover:bg-yellow-400 dark:focus:ring-yellow-900", }, + ghostColor: { + default: + "bg-transparent text-primary-700 hover:bg-primary-100 focus:ring-primary-300 dark:text-primary-500 dark:hover:bg-primary-900/50 dark:focus:ring-primary-800", + alternative: + "bg-transparent text-gray-900 hover:bg-gray-100 focus:ring-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-700", + blue: "bg-transparent text-blue-700 hover:bg-blue-100 focus:ring-blue-300 dark:text-blue-500 dark:hover:bg-blue-900/50 dark:focus:ring-blue-800", + cyan: "bg-transparent text-cyan-700 hover:bg-cyan-100 focus:ring-cyan-300 dark:text-cyan-500 dark:hover:bg-cyan-900/50 dark:focus:ring-cyan-800", + dark: "bg-transparent text-gray-800 hover:bg-gray-100 focus:ring-gray-300 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-gray-700", + gray: "bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-300 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-800", + green: + "bg-transparent text-green-700 hover:bg-green-100 focus:ring-green-300 dark:text-green-500 dark:hover:bg-green-900/50 dark:focus:ring-green-800", + indigo: + "bg-transparent text-indigo-700 hover:bg-indigo-100 focus:ring-indigo-300 dark:text-indigo-400 dark:hover:bg-indigo-900/50 dark:focus:ring-indigo-800", + light: + "bg-transparent text-gray-500 hover:bg-gray-100 focus:ring-gray-100 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-gray-700", + lime: "bg-transparent text-lime-700 hover:bg-lime-100 focus:ring-lime-300 dark:text-lime-500 dark:hover:bg-lime-900/50 dark:focus:ring-lime-800", + pink: "bg-transparent text-pink-700 hover:bg-pink-100 focus:ring-pink-300 dark:text-pink-500 dark:hover:bg-pink-900/50 dark:focus:ring-pink-800", + purple: + "bg-transparent text-purple-700 hover:bg-purple-100 focus:ring-purple-300 dark:text-purple-400 dark:hover:bg-purple-900/50 dark:focus:ring-purple-800", + red: "bg-transparent text-red-700 hover:bg-red-100 focus:ring-red-300 dark:text-red-500 dark:hover:bg-red-900/50 dark:focus:ring-red-900", + teal: "bg-transparent text-teal-700 hover:bg-teal-100 focus:ring-teal-300 dark:text-teal-400 dark:hover:bg-teal-900/50 dark:focus:ring-teal-800", + yellow: + "bg-transparent text-yellow-500 hover:bg-yellow-100 focus:ring-yellow-300 dark:text-yellow-300 dark:hover:bg-yellow-900/50 dark:focus:ring-yellow-900", + }, outlineColor: { default: "border border-primary-700 text-primary-700 hover:border-primary-800 hover:bg-primary-800 hover:text-white focus:ring-primary-300 dark:border-primary-600 dark:text-primary-500 dark:hover:border-primary-700 dark:hover:bg-primary-700 dark:hover:text-white dark:focus:ring-primary-800", @@ -54,7 +78,7 @@ export const buttonTheme = createTheme({ pink: "border border-pink-700 text-pink-700 hover:border-pink-800 hover:bg-pink-800 hover:text-white focus:ring-pink-300 dark:border-pink-600 dark:text-pink-500 dark:hover:border-pink-700 dark:hover:bg-pink-700 dark:hover:text-white dark:focus:ring-pink-800", purple: "border border-purple-700 text-purple-700 hover:border-purple-800 hover:bg-purple-800 hover:text-white focus:ring-purple-300 dark:border-purple-600 dark:text-purple-400 dark:hover:border-purple-700 dark:hover:bg-purple-700 dark:hover:text-white dark:focus:ring-purple-800", - red: "border border-red-700 text-red-700 hover:border-red-800 hover:bg-red-800 hover:text-white focus:ring-red-300 dark:border-red-600 dark:text-red-500 dark:hover:border-red-700 dark:hover:bg-red-700 dark:hover:text-white dark:focus:ring-red-800", + red: "border border-red-700 text-red-700 hover:border-red-800 hover:bg-red-800 hover:text-white focus:ring-red-300 dark:border-red-600 dark:text-red-500 dark:hover:border-red-700 dark:hover:bg-red-700 dark:hover:text-white dark:focus:ring-red-900", teal: "border border-teal-700 text-teal-700 hover:border-teal-800 hover:bg-teal-800 hover:text-white focus:ring-teal-300 dark:border-teal-600 dark:text-teal-400 dark:hover:border-teal-700 dark:hover:bg-teal-700 dark:hover:text-white dark:focus:ring-teal-800", yellow: "border border-yellow-400 text-yellow-400 hover:border-yellow-500 hover:bg-yellow-500 hover:text-white focus:ring-yellow-300 dark:border-yellow-300 dark:text-yellow-300 dark:hover:border-yellow-400 dark:hover:bg-yellow-400 dark:hover:text-white dark:focus:ring-yellow-900", From ab975d05f7ca6fd1509c8df97a7eb8553fc223cd Mon Sep 17 00:00:00 2001 From: Mohamed Anwar Date: Mon, 10 Aug 2026 03:49:15 +0300 Subject: [PATCH 2/2] chore: keep ghost button changes scoped --- packages/ui/src/components/Button/theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/Button/theme.ts b/packages/ui/src/components/Button/theme.ts index dc44a72e12..fd5a9cbc20 100644 --- a/packages/ui/src/components/Button/theme.ts +++ b/packages/ui/src/components/Button/theme.ts @@ -34,7 +34,7 @@ export const buttonTheme = createTheme({ pink: "bg-pink-700 text-white hover:bg-pink-800 focus:ring-pink-300 dark:bg-pink-600 dark:hover:bg-pink-700 dark:focus:ring-pink-800", purple: "bg-purple-700 text-white hover:bg-purple-800 focus:ring-purple-300 dark:bg-purple-600 dark:hover:bg-purple-700 dark:focus:ring-purple-800", - red: "bg-red-700 text-white hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900", + red: "bg-red-700 text-white hover:bg-red-800 focus:ring-red-300 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800", teal: "bg-teal-700 text-white hover:bg-teal-800 focus:ring-teal-300 dark:bg-teal-600 dark:hover:bg-teal-700 dark:focus:ring-teal-800", yellow: "bg-yellow-400 text-white hover:bg-yellow-500 focus:ring-yellow-300 dark:bg-yellow-600 dark:hover:bg-yellow-400 dark:focus:ring-yellow-900", @@ -78,7 +78,7 @@ export const buttonTheme = createTheme({ pink: "border border-pink-700 text-pink-700 hover:border-pink-800 hover:bg-pink-800 hover:text-white focus:ring-pink-300 dark:border-pink-600 dark:text-pink-500 dark:hover:border-pink-700 dark:hover:bg-pink-700 dark:hover:text-white dark:focus:ring-pink-800", purple: "border border-purple-700 text-purple-700 hover:border-purple-800 hover:bg-purple-800 hover:text-white focus:ring-purple-300 dark:border-purple-600 dark:text-purple-400 dark:hover:border-purple-700 dark:hover:bg-purple-700 dark:hover:text-white dark:focus:ring-purple-800", - red: "border border-red-700 text-red-700 hover:border-red-800 hover:bg-red-800 hover:text-white focus:ring-red-300 dark:border-red-600 dark:text-red-500 dark:hover:border-red-700 dark:hover:bg-red-700 dark:hover:text-white dark:focus:ring-red-900", + red: "border border-red-700 text-red-700 hover:border-red-800 hover:bg-red-800 hover:text-white focus:ring-red-300 dark:border-red-600 dark:text-red-500 dark:hover:border-red-700 dark:hover:bg-red-700 dark:hover:text-white dark:focus:ring-red-800", teal: "border border-teal-700 text-teal-700 hover:border-teal-800 hover:bg-teal-800 hover:text-white focus:ring-teal-300 dark:border-teal-600 dark:text-teal-400 dark:hover:border-teal-700 dark:hover:bg-teal-700 dark:hover:text-white dark:focus:ring-teal-800", yellow: "border border-yellow-400 text-yellow-400 hover:border-yellow-500 hover:bg-yellow-500 hover:text-white focus:ring-yellow-300 dark:border-yellow-300 dark:text-yellow-300 dark:hover:border-yellow-400 dark:hover:bg-yellow-400 dark:hover:text-white dark:focus:ring-yellow-900",