Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/web/content/docs/components/alert.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ You can use the `onDismiss` prop on the `<Alert>` component to add a dismiss but

<Example name="alert.dismissible" />

## Dismiss transition

Animate the alert while it is being dismissed by passing the `transition`, `timing` and `duration` props, mirroring the [options of vanilla Flowbite's `Dismiss` class](https://flowbite.com/docs/components/alerts/#options). `onDismiss` fires once the transition has finished.

- `transition` — a Tailwind CSS transition utility class (default: `transition-opacity`)
- `timing` — a Tailwind CSS transition-timing-function utility class (default: `ease-out`)
- `duration` — the length of the transition in milliseconds (default: `300`)

<Example name="alert.transition" />

## Rounded alert

To make the alert box rounded you can use the `rounded` prop on the `<Alert>` component.
Expand Down
63 changes: 63 additions & 0 deletions apps/web/examples/alert/alert.transition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client";

import { Alert } from "flowbite-react";
import { useState } from "react";
import type { CodeData } from "~/components/code-demo";

const code = `
"use client";

import { useState } from "react";
import { Alert } from "flowbite-react";

export function Component() {
const [isDismissed, setIsDismissed] = useState(false);

if (isDismissed) {
return null;
}

return (
<Alert
color="success"
duration={1000}
onDismiss={() => setIsDismissed(true)}
timing="ease-in-out"
transition="transition-opacity"
>
<span className="font-medium">Success alert!</span> Dismiss me to see the fade transition.
</Alert>
);
}
`;

export function Component() {
const [isDismissed, setIsDismissed] = useState(false);

if (isDismissed) {
return null;
}

return (
<Alert
color="success"
duration={1000}
onDismiss={() => setIsDismissed(true)}
timing="ease-in-out"
transition="transition-opacity"
>
<span className="font-medium">Success alert!</span> Dismiss me to see the fade transition.
</Alert>
);
}

export const transition: CodeData = {
type: "single",
code: {
fileName: "index",
language: "tsx",
code,
},
githubSlug: "alert/alert.transition.tsx",
component: <Component />,
};
1 change: 1 addition & 0 deletions apps/web/examples/alert/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export { borderAccent } from "./alert.borderAccent";
export { dismissible } from "./alert.dismissible";
export { root } from "./alert.root";
export { rounded } from "./alert.rounded";
export { transition } from "./alert.transition";
export { withIcon } from "./alert.withIcon";
70 changes: 67 additions & 3 deletions packages/ui/src/components/Alert/Alert.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,75 @@ describe("Components / Alert", () => {
});
});

describe("Dismiss transition", () => {
it("should apply the default transition classes while dismissing", async () => {
const user = userEvent.setup();
render(<Alert onDismiss={vi.fn()} />);

await user.click(dismiss());

expect(alert()).toHaveClass("transition-opacity", "duration-300", "ease-out", "opacity-0");
});

it("should fire `onDismiss` after the `duration` has elapsed", async () => {
const onDismiss = vi.fn();
const user = userEvent.setup();
render(<Alert duration={50} onDismiss={onDismiss} />);

await user.click(dismiss());

expect(onDismiss).not.toHaveBeenCalled();

await waitFor(() => expect(onDismiss).toHaveBeenCalledTimes(1));
});

it("should use the custom `transition`, `timing` and `duration` props", async () => {
const user = userEvent.setup();
render(<Alert duration={1000} onDismiss={vi.fn()} timing="ease-in" transition="transition-transform" />);

await user.click(dismiss());

expect(alert()).toHaveClass("transition-transform", "duration-1000", "ease-in", "opacity-0");
});

it("should honour a custom `transition` theme", async () => {
const theme: CustomFlowbiteTheme = {
alert: {
transition: {
base: "transition-transform",
duration: "duration-700",
timing: "ease-in-out",
},
},
};
const user = userEvent.setup();
render(
<ThemeProvider theme={theme}>
<Alert onDismiss={vi.fn()} />
</ThemeProvider>,
);

await user.click(dismiss());

expect(alert()).toHaveClass("transition-transform", "duration-700", "ease-in-out", "opacity-0");
});

it("should fire `onDismiss` immediately when `duration` is 0", async () => {
const onDismiss = vi.fn();
const user = userEvent.setup();
render(<Alert duration={0} onDismiss={onDismiss} />);

await user.click(dismiss());

expect(onDismiss).toHaveBeenCalledTimes(1);
});
});

describe("Keyboard interactions", () => {
it("should dismiss when `Tab` is pressed to navigate to Dismiss button and `Space` is pressed", async () => {
const onDismiss = vi.fn();
const user = userEvent.setup();
render(<Alert onDismiss={onDismiss} />);
render(<Alert duration={0} onDismiss={onDismiss} />);

await waitFor(async () => {
await user.tab();
Expand All @@ -142,10 +206,10 @@ describe("Components / Alert", () => {
});

describe("Props", () => {
it("should call `onDismiss` when clicked", async () => {
it("should call `onDismiss` when clicked (after the dismiss transition)", async () => {
const onDismiss = vi.fn();
const user = userEvent.setup();
render(<Alert onDismiss={onDismiss} />);
render(<Alert duration={0} onDismiss={onDismiss} />);

await user.click(dismiss());

Expand Down
47 changes: 45 additions & 2 deletions packages/ui/src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { forwardRef, type ComponentProps, type FC, type ReactNode } from "react";
import { forwardRef, useEffect, useState, type ComponentProps, type FC, type ReactNode } from "react";
import { get } from "../../helpers/get";
import { resolveProps } from "../../helpers/resolve-props";
import { useResolveTheme } from "../../helpers/resolve-theme";
Expand All @@ -17,6 +17,7 @@
color: FlowbiteColors;
icon: string;
rounded: string;
transition?: Partial<AlertTransitionTheme>;
wrapper: string;
}

Expand All @@ -26,12 +27,21 @@
icon: string;
}

export interface AlertTransitionTheme {
base: string;
duration: string;
timing: string;
}

export interface AlertProps extends Omit<ComponentProps<"div">, "color">, ThemingProps<AlertTheme> {
additionalContent?: ReactNode;
color?: DynamicStringEnumKeysOf<FlowbiteColors>;
duration?: number;
icon?: FC<ComponentProps<"svg">>;
onDismiss?: ComponentProps<"button">["onClick"];
rounded?: boolean;
timing?: string;
transition?: string;
withBorderAccent?: boolean;
}

Expand All @@ -48,13 +58,36 @@
children,
className,
color = "info",
duration = 300,
icon: Icon,
onDismiss,
rounded = true,
timing = "ease-out",
transition = "transition-opacity",
withBorderAccent,
...restProps
} = resolveProps(props, provider.props?.alert);

const [isDismissing, setIsDismissing] = useState(false);

useEffect(() => {
if (!isDismissing) {
return;
}
const timeoutId = setTimeout(() => {
onDismiss?.(new MouseEvent("click") as unknown as React.MouseEvent<HTMLButtonElement>);
}, duration);
Comment on lines +93 to +95

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the Alert component around the reported lines.
if git ls-files 'packages/ui/src/components/Alert/Alert.tsx' | grep -q .; then
  echo "== file exists =="
  wc -l packages/ui/src/components/Alert/Alert.tsx
  echo "== relevant lines =="
  cat -n packages/ui/src/components/Alert/Alert.tsx | sed -n '1,180p'
else
  echo "Alert.tsx not found"
  fd -i 'Alert\.tsx$' .
fi

echo "== search onDismiss usages =="
rg -n "onDismiss|new MouseEvent|onDismiss\?\(" packages/ui/src || true

echo "== inspect package/tool config maybe relevant =="
fd -i "package.json|tailwind.config" . | sed -n '1,80p'

Repository: themesberg/flowbite-react

Length of output: 8677


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect related test around onDismiss assertions =="
cat -n packages/ui/src/components/Alert/Alert.test.tsx | sed -n '120,225p'

echo "== TypeScript check around mock/expect behavior without compiling repo code? =="
echo "node version:"
node --version || true
node - <<'JS'
class SyntheticEvent {
  constructor(type, target, currentTarget) {
    this.type = type;
    this.target = target;
    this.currentTarget = currentTarget;
  }
}
const native = new MouseEvent("click");
const cast = native;
console.log(JSON.stringify({
  type: cast.type,
  target: cast.target,
  currentTarget: cast.currentTarget,
  stopPropagationType: typeof cast.stopPropagation,
}, null, 2));
console.log(JSON.stringify({
  syn: new SyntheticEvent("click", "button", "root").target,
  eventTarget: cast.target,
}, null, 2));
JS

Repository: themesberg/flowbite-react

Length of output: 4767


Pass the original dismiss click event to the deferred onDismiss callback.

The timed callback currently receives a fabricated native MouseEvent, so callers won’t get a React synthetic click event with the correct target/currentTarget. Keep the event from handleDismissClick and pass it through when the timeout runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/Alert/Alert.tsx` around lines 93 - 95, Update the
dismiss flow in Alert’s handleDismissClick and timeout callback to retain the
original React mouse event and pass that same event to onDismiss when the timer
fires, removing the fabricated MouseEvent while preserving the existing duration
behavior.

return () => clearTimeout(timeoutId);
}, [isDismissing, duration, onDismiss]);
Comment on lines +73 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate Alert files =="
fd -a 'Alert\.(tsx|ts|jsx|js)$' . | sed 's#^\./##'

echo "== target file outline/size =="
wc -l packages/ui/src/components/Alert/Alert.tsx 2>/dev/null || true
ast-grep outline packages/ui/src/components/Alert/Alert.tsx 2>/dev/null || true

echo "== target file relevant contents =="
cat -n packages/ui/src/components/Alert/Alert.tsx | sed -n '1,140p'

echo "== usages of Alert =="
rg -n "<Alert|Alert " packages -g '*.{ts,tsx,js,jsx}' | head -120

Repository: themesberg/flowbite-react

Length of output: 8034


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Alert tests relevant contents =="
cat -n packages/ui/src/components/Alert/Alert.test.tsx | sed -n '1,260p'

echo "== package config for jsdom timers/react =="
for f in package.json packages/ui/package.json; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    node -e "const p=require('./$f'); console.log(JSON.stringify({scripts:p.scripts,dependencies:p.dependencies,devDependencies:p.devDependencies}, null, 2))" | sed -n '1,220p'
  fi
done

echo "== focused searches for tests around dismissing/transition/inline callbacks =="
rg -n "onDismiss|dismiss|transition|duration|setInterval|setTimeout|re-render|rerender|render\(\\\(" packages/ui/src/components/Alert packages/ui/src/components -g '!**/*.js' -g '!**/*.map' | sed -n '1,240p'

Repository: themesberg/flowbite-react

Length of output: 13891


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== vitest config =="
fd -a '^(vitest|vite|rollup|rslib)\.(config|configuration)\.(ts|js|mjs|cjs)$' packages/ui | sort | while read -r f; do
  echo "-- $f --"
  cat -n "$f" | sed -n '1,220p'
done

echo "== focused Alert searches =="
rg -n "onDismiss|dismiss|transition|duration|setInterval|setTimeout|re-render|rerender|render\\(" packages/ui/src/components/Alert packages/ui/src/components -g '!**/*.js' -g '!**/*.map' | sed -n '1,260p'

echo "== React dependency versions =="
node - <<'JS'
const fs = require('fs');
for (const f of ['package.json','packages/ui/package.json']) {
  if (!fs.existsSync(f)) continue;
  const p = JSON.parse(fs.readFileSync(f,'utf8'));
  const deps = {...(p.dependencies||{}), ...(p.devDependencies||{})};
  console.log(f, deps.react, deps['react-dom']);
}
JS

Repository: themesberg/flowbite-react

Length of output: 31536


🌐 Web query:

React useEffect cleanup timeout rerender clearTimeout setTimeout dependency changes behavior

💡 Result:

In React, the useEffect cleanup function plays a critical role in managing side effects that require stopping, cancelling, or undoing operations [1][2]. When using setTimeout (or similar async operations like subscriptions or intervals) within useEffect, the cleanup function is essential to ensure that pending operations from a previous render do not interfere with subsequent ones [2][3]. React guarantees the following behavior regarding cleanup execution [2][4]: 1. Timing of Cleanup: React always executes the cleanup function from the previous render before running the effect function for the next render [2][3]. If a component unmounts, the cleanup function is executed one final time [1][2]. 2. Dependency Changes: If you provide a dependency array, React only re-synchronizes the effect (running the cleanup, then the new effect) when one of the values in that array changes [5][4]. If the dependency array is empty ([]), the effect runs only once after the initial mount, and the cleanup runs only when the component unmounts [2][4]. 3. Interaction with setTimeout: When you use setTimeout within an effect, returning clearTimeout(timeoutId) in the cleanup function is the standard pattern to prevent the timeout callback from executing if the component updates or unmounts before the timer expires [2]. Because React cleans up the previous render's effect before starting the next one, this mechanism ensures that at most one timeout is active for that effect at any given time [2]. If you find that your effect is running too often—such as on every render—it is typically because the dependency array is missing or includes values that change on every render [5][6]. You can avoid this by either ensuring the dependencies are stable or by using functional state updates (e.g., setCount(c => c + 1)) to remove the state variable from the effect's dependency list entirely [7][8]. In summary, the cleanup function does not run on every render by default; it only runs when the dependencies change or the component unmounts [2][6]. By cleaning up the previous timeout before establishing a new one, you maintain predictable behavior and avoid race conditions or memory leaks [2][3].

Citations:


Keep the dismissal deadline fixed at click time.

The dismiss effect depends on onDismiss, so an inline dismissal callback causes the timeout to be cleared and restarted on every parent rerender during the CSS transition; a constant callback/deadline should be captured when clicked, with a rerender-during-transition regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/Alert/Alert.tsx` around lines 73 - 81, Update the
Alert dismissal flow around the useEffect so the timeout deadline is captured
once when dismissal begins and is not reset when onDismiss changes during the
transition. Store or otherwise stabilize the clicked dismissal
callback/deadline, keep the effect from restarting on parent rerenders caused by
inline callbacks, and add a regression test covering rerenders during the CSS
transition.


const handleDismissClick: ComponentProps<"button">["onClick"] = (event) => {
if (transition || duration > 0) {
setIsDismissing(true);
return;
}
onDismiss?.(event);
Comment on lines +99 to +105

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make duration={0} synchronous.

transition defaults to "transition-opacity", so this condition is true even when duration is 0. The callback is therefore deferred through setTimeout(..., 0), contrary to the documented immediate behavior. The test's await user.click(...) masks this ordering bug.

Use duration > 0 as the deferral condition, and add a synchronous assertion for the zero-duration case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/Alert/Alert.tsx` around lines 99 - 105, Update
handleDismissClick in Alert to defer dismissal only when duration > 0, so
duration={0} invokes onDismiss synchronously even when the default transition is
present. Add or update the zero-duration test with a synchronous assertion
immediately after the click rather than relying on awaiting user.click.

};

return (
<div
ref={ref}
Expand All @@ -63,6 +96,16 @@
theme.color[color],
rounded && theme.rounded,
withBorderAccent && theme.borderAccent,
isDismissing &&
twMerge(

Check warning on line 100 in packages/ui/src/components/Alert/Alert.tsx

View workflow job for this annotation

GitHub Actions / 🕵 Lint

Classname 'duration-' is not a Tailwind CSS class!
transition,
`duration-${duration}`,
timing,
Comment on lines +117 to +120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)(Alert\.tsx|tailwind|postcss|package\.json|\.lint|eslint|stylelint|.*\.config\.(js|ts|mjs|cjs))$|Alert' || true

echo "== target snippet =="
if [ -f packages/ui/src/components/Alert/Alert.tsx ]; then
  nl -ba packages/ui/src/components/Alert/Alert.tsx | sed -n '1,180p'
fi

echo "== duration/static Tailwind config candidates =="
rg -n "duration-|transitionDuration| safelist|content|preset|plugins|tailwindcss" -S . -g '!node_modules' -g '!dist' -g '!build' -g '!coverage' | head -200

echo "== lint configs mentioning duration =="
rg -n "duration|transition|valid|invalid" -S . -g '*lnt*' -g '!node_modules' -g '!dist' -g '!build' | head -200 || true

Repository: themesberg/flowbite-react

Length of output: 1043


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Alert.tsx relevant lines =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ui/src/components/Alert/Alert.tsx | sed -n '105,130p'

echo "== Alert.tsx duration prop/types =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ui/src/components/Alert/Alert.tsx | sed -n '1,120p'

echo "== tailwind config duration/content =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ui/tailwind.config.cjs
echo "---"
awk '{printf "%6d\t%s\n", NR, $0}' apps/web/tailwind.config.cjs | sed -n '1,260p'

echo "== runtime duration values in Alert.tsx/usages =="
rg -n "duration|transitionDuration|twMerge\\(|duration-" packages/ui/src/components/Alert packages/ui/tailwind.config.cjs -S

echo "== lint rules mentioning invalid transition/duration =="
rg -n "transitionDuration|duration-|valid|Invalid|invalid|CSS variable|arbitrary" -S . -g '!node_modules' | head -200

Repository: themesberg/flowbite-react

Length of output: 40429


🌐 Web query:

Tailwind CSS arbitrary values duration- arbitrary value transition duration

💡 Result:

To apply an arbitrary transition duration in Tailwind CSS, use the square bracket syntax directly within the class name [1][2]. For example:

If you are using a CSS variable, you can use the same syntax [1]:
Alternatively, Tailwind provides a shorthand syntax for CSS variables using parentheses, which automatically wraps your value in the var function [1]:
This functionality allows you to generate one-off transition durations on the fly without needing to extend your theme configuration [2][3]. As with all arbitrary values, this syntax integrates seamlessly with responsive modifiers (e.g., md:duration-[500ms]) and interactive states (e.g., hover:duration-[1s]) [1][4][3].

Citations:


Use arbitrary transition duration values.

duration-${duration} generates classes like duration-300, which Tailwind does not treat as arbitrary values and won’t include for non-default/custom durations. Use duration-[${duration}ms] or duration-[var(--alert-duration)] instead.

🧰 Tools
🪛 GitHub Check: 🕵 Lint

[warning] 117-117:
Classname 'duration-' is not a Tailwind CSS class!

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/Alert/Alert.tsx` around lines 117 - 120, Update
the transition class construction in the Alert component’s twMerge call to use
Tailwind’s arbitrary duration syntax, such as duration-[${duration}ms] or the
existing alert-duration CSS variable, so custom durations are applied correctly.

Source: Linters/SAST tools

theme.transition?.base,
theme.transition?.duration,
theme.transition?.timing,
Comment on lines +121 to +123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep the callback delay aligned with themed duration.

A theme can override the rendered class to duration-700, but the timeout still uses the prop default of 300. onDismiss can unmount the alert before its themed transition finishes. Model the effective duration as one shared numeric value for both CSS and the timer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/Alert/Alert.tsx` around lines 121 - 123, Update
the Alert component’s transition handling to derive one effective numeric
duration from the themed transition value, including overrides such as
duration-700, and reuse that value for both the rendered transition class and
the onDismiss timeout. Replace the independent prop-default timer usage while
preserving existing callback behavior.

"opacity-0",
),
className,
)}
role="alert"
Expand All @@ -75,7 +118,7 @@
<button
aria-label="Dismiss"
className={twMerge(theme.closeButton.base, theme.closeButton.color[color])}
onClick={onDismiss}
onClick={handleDismissClick}
type="button"
>
<XIcon aria-hidden className={theme.closeButton.icon} />
Expand Down
Loading