Facetcomponents

Alert

Three parts, one recipe object, and a variant repeated on every part that draws.

Terminal window
npx facet-rbxts add alert

Copies ui/alert.tsx, plus lib/utils.ts. Needs @facet-ui/react-variants and @lattice-ui/react-runtime.

import { Alert, AlertTitle, AlertDescription } from "../shared/ui/alert";
<Alert>
<AlertTitle Text="Saved" />
<AlertDescription Text="Your loadout is stored on the server." />
</Alert>
Both variants. Note that destructive is stated three times, once per part.

The parts

PartRendersClasses
AlertFrameflex-col w-full h-fit gap-1 rounded-lg border p-4
AlertTitleTextLabelw-full h-fit whitespace-normal text-left text-sm font-medium
AlertDescriptionTextLabelw-full h-fit whitespace-normal leading-tight text-left text-xs font-normal

Flat named exports, like Card — this is source you paste and edit, so each part reads and deletes on its own.

Renders a Frame. Unknown props forward onto it and are type-checked against it, so a prop Frame does not accept is a compile error.

Alert forwards onto a Frame and takes children. The two text parts forward onto a TextLabel and take Text.

Props

PropTypeDescription
variant"default" | "destructive"Takes it on every part, not just the root. Defaults to default. See below — this is the thing to notice if you are coming from shadcn.
TextstringText parts only. Drawn as the instance's own Text — these are leaves.
childrenReact.ReactNodeRoot only.
classNameClassNameThreaded into the recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site.

variant goes on every part

This is the one surprise in the component, and it is not an oversight:

<Alert variant="destructive">
<AlertTitle variant="destructive" Text="Kicked" />
<AlertDescription variant="destructive" Text="You were removed from the server." />
</Alert>

In shadcn the variant is set once on the root and a descendant selector colours the title. Roblox has no descendant selectors and nothing inherits, so variant has to reach every part that draws something.

Verbose on purpose. The alternative is a React context you then own and keep wired through your own edits, for a component whose whole body is three instances. If the repetition annoys you, the file is yours — add the context, or collapse the parts into one component taking two strings.

variantRootTitleDescription
defaultbg-card border-bordertext-card-foregroundtext-muted-foreground
destructivebg-card border-destructivetext-destructivetext-destructive

The destructive surface is the same bg-card as the default. Only the border and text change. A red fill behind body text is a contrast problem the theme cannot solve, and the border carries the signal alone.

Three parts, one recipe object

export const alertVariants = {
root: fv("flex-col w-full h-fit gap-1 rounded-lg border p-4", { variants: { … } }),
title: fv("w-full h-fit whitespace-normal text-left text-sm font-medium", { variants: { … } }),
description: fv("w-full h-fit whitespace-normal leading-tight text-left text-xs font-normal", { variants: { … } }),
};

One object rather than three export consts, for the reason Card is written the same way. Every exported name costs a Luau register, and card once went over the 200-register limit and stopped loading. Three parts is nowhere near that ceiling — the shape is the convention, kept because it is free.

See the register limit.

w-full h-fit, all the way down

Every part carries it, exactly as card does: width from the parent, height from the content. A part that fails to resolve a height collapses the alert above it.

whitespace-normal and text-left are on both text parts, because Roblox centres text and keeps it on one line by default. That matters most for a description, the part most likely to wrap.

font-normal on the description is not redundant beside font-medium on the title. Vela leaves FontFace alone when no font-* appears, and Roblox’s default is LegacyArial. That is the bug that shipped in card.

The root keeps its background

<frame
className={cn(alertVariants.root({ variant, className }))}
BorderSizePixel={0}
…
/>

No BackgroundTransparency: 1 on the root, unlike most components — the surface is the alert. Only Roblox’s 1px border is cleared, since border in the recipe draws the real one. The text parts do clear their backgrounds, or a TextLabel’s opaque default would paint a box behind every line.

That is the same rule Separator follows, reached from the other direction.