# rapidUI – full documentation
> Accessible, themeable React components for the Rapid Development Framework. Built on shadcn/ui and Base UI, installed with the shadcn CLI.
## Setup
rapidUI is a shadcn/ui registry. Components are copied into the project as source (`components/ui/*`), not installed as a package. Requires React 19, Tailwind CSS v4 and a `components.json` (run `npx shadcn@latest init` first if it is missing).
1. Apply the Rapid theme (CSS variables, fonts, base styles):
`npx shadcn@latest init https://rdf.dev.360online.com/r/styles/rapid.json`
2. Add a component:
`npx shadcn@latest add https://rdf.dev.360online.com/r/button.json`
3. Import it:
`import { Button } from '@/components/ui/button'`
Optional: register the namespace in `components.json` to use short names (`npx shadcn@latest add @rapid/button`):
```json
{ "registries": { "@rapid": "https://rdf.dev.360online.com/r/{name}.json" } }
```
Rules for agents:
- Install with the shadcn CLI instead of writing components by hand; dependencies between components resolve automatically.
- Use theme tokens (`bg-primary`, `text-muted-foreground`, `border-border`, …) rather than raw colours.
- The theme uses shadcn's CSS variables plus `--success` and `--warning` (with `-foreground`), so any shadcn component or example works unchanged. Use `text-success`, `bg-warning/10` and so on for status colours. Buttons and fields are 40px by default (32px for `sm`), text 14px and badges 12px.
- Components are built on Base UI: compose with the `render` prop (e.g. `}>`), there is no `asChild`. Changes from the Radix version: https://rdf.dev.360online.com/docs/base-ui/
---
# Accordion
Vertically stacked, collapsible sections built on Base UI Accordion.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/accordion.json`
Import from: `@/components/ui/accordion`
npm dependencies: @base-ui/react@^1.8.0, lucide-react@^1.48.0
A vertically stacked set of interactive headings that each reveal a section of content.
Built on top of [Base UI Accordion](https://base-ui.com/react/components/accordion),
it follows WAI-ARIA design patterns and animates open and closed.
### Features
- One expanded section at a time by default, or several with `multiple`
- Every open section can be closed again
- Height animation on open/close (`animate-accordion-down` / `animate-accordion-up`)
- Chevron indicator that switches from down to up when the item is expanded
- Items are separated by a bottom border (except the last one)
- Content styles for rich text: links are underlined and consecutive paragraphs are spaced
- Disabled items
- Keyboard navigation and screen reader support
- `data-slot` attributes (`accordion`, `accordion-item`, `accordion-trigger`, `accordion-content`) for styling hooks
### Usage
```tsx
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from '@/components/ui/accordion'
// One section open at a time
export function PermitFaq() {
return (
Do I need a building permit for a garden shed?Not if it is smaller than 15 m² and lower than 3 metres.How long does it take to get a decision?Most complete applications are decided within 4 weeks.
)
}
// Several sections open at once
export function CaseDetails() {
return (
ApplicantAnna LindqvistPropertyEken 4:12, Storgatan 14
)
}
```
#### Default
A FAQ for a building permit e-service. By default one answer is open at a time, and `defaultValue={['need-permit']}` opens the first one. `value` and `defaultValue` are always arrays.
#### Multiple Sections
With `multiple` several sections can be open at once, which suits a case summary where the handling officer
compares applicant and property details side by side.
#### Bordered
Wrap the accordion in a border and pad the items with `className` for a card-like list. Triggers can hold an icon next
to the label.
#### Disabled State
Disable an `AccordionItem` for a step that is not available yet, here the offer in a preschool placement application.
#### Rich Content
Paragraphs and links inside `AccordionContent` get spacing and underline styles automatically, and you can add actions
such as a button.
### Props
#### Accordion
- `multiple` - Whether several items can be open at once (default `false`)
- `defaultValue` - The values of the items to open by default (an array, also when only one item can be open)
- `value` - The controlled values of the open items (an array)
- `onValueChange` - Event handler for when the opened items change, `(value, eventDetails) => void`
- `hiddenUntilFound` - Keep closed panels findable with the browser's find-in-page, which opens the matching item
- `disabled` - Disables every item
- `className` - Additional CSS classes (the root is a full-width flex column)
#### AccordionItem
- `value` - Unique identifier for the item
- `disabled` - Whether the item is disabled
- `className` - Additional CSS classes
#### AccordionTrigger
- `children` - Content to display in the trigger. A chevron icon is appended automatically.
- `headingLevel` - The heading level wrapping the trigger: `2`, `3` or `4` (default `3`). Pick the level that follows the heading above the accordion, so the page outline skips no level, for example `2` when it sits directly under the page's H1.
- `className` - Additional CSS classes
#### AccordionContent
- `children` - Content to display when expanded
- `className` - Additional CSS classes
### Accessibility
The Accordion component follows WAI-ARIA guidelines:
- Uses proper ARIA roles (`region`, `button`)
- Supports keyboard navigation:
- `Space/Enter`: Toggle section
- `Tab`: Navigate between sections
- `Home/End`: Move to first/last section
- Announces state changes to screen readers
- Maintains focus management
- Provides visual feedback for interaction states
### Best Practices
1. Content Organization:
- Group related content together
- Use clear, descriptive headers
- Keep content concise and focused
2. Visual Hierarchy:
- Use consistent spacing
- Maintain clear section boundaries
- Provide visual feedback for state changes
3. Accessibility:
- Write descriptive trigger text
- Ensure keyboard navigation
- Maintain sufficient touch targets
4. Performance:
- Lazy load content when possible
- Use appropriate animations
- Consider content height changes
5. User Experience:
- Allow section collapse when appropriate
- Provide smooth transitions
- Consider default open states
### Related Components
- Tabs
- Collapsible
## Source
File: components/ui/accordion.tsx
```tsx
'use client'
import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion'
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
return
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return
}
interface AccordionTriggerProps extends AccordionPrimitive.Trigger.Props {
/** The heading level wrapping the trigger, to fit the page's outline. Defaults to 3. */
headingLevel?: 2 | 3 | 4
}
function AccordionTrigger({ className, children, headingLevel = 3, ...props }: Readonly) {
const Heading = `h${headingLevel}` as const
return (
}>
{children}
)
}
function AccordionContent({ className, children, ...props }: AccordionPrimitive.Panel.Props) {
return (
{/* No fixed height here: the open/close keyframes on the panel use --accordion-panel-height, and at rest the
content keeps its natural height so it can reflow (zoom, text spacing, content that grows) */}
{children}
)
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
```
---
# Alert
Inline callout for status messages, with default and destructive variants and an optional action.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/alert.json`
Import from: `@/components/ui/alert`
npm dependencies: class-variance-authority@^0.7.1
A versatile alert component for displaying callouts and inline messages. Built with class-variance-authority and styled with Tailwind CSS for consistent visual hierarchy.
### Features
- Five variants: `default`, `destructive`, `info`, `success` and `warning`
- Optional leading icon: place an `svg` as the first child and the layout switches to a two-column grid
- Composable `AlertTitle`, `AlertDescription` and `AlertAction`
- `AlertAction` pins a button to the top-right corner
- Links inside title and description are underlined automatically
- A `role` that follows the variant: `alert` for `destructive`, `status` for the others. Override it with the `role` prop
- `data-slot` attributes on every part
### Usage
```tsx
import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { CalendarClockIcon, WrenchIcon } from 'lucide-react'
export function ServiceNotices() {
return (
{/* Simple alert */}
Planned maintenance on Saturday
E-services will be unavailable on 14 March 2026 between 22:00 and 02:00.
{/* With action */}
Your parking permit expires in 12 daysResident permit for zone C, valid until 10 April 2026.
)
}
```
#### Default Alert
A neutral notice about planned maintenance of the e-services.
#### Variants
`default` for neutral notices, `info` for information worth noticing, `success` for confirmations, `warning` for things
the user should act on soon, and `destructive` for errors that stop them.
#### Status Messages
Service messages with the `success`, `info` and `warning` variants. The tinted background and icon colour give each
message its tone, but the icon and title carry the meaning, so status never relies on colour alone.
```tsx
Water outage on KungsgatanKungsgatan 1–27 is without water until about 15:00.
```
#### Roles
The `role` decides how a screen reader announces the message:
| Role | Default for | Announcement | Use it for |
| --- | --- | --- | --- |
| `alert` | `destructive` | At once, interrupting what is being read | Errors and urgent messages that appear after an action: a failed submission, a session about to end |
| `status` | `default`, `info`, `success`, `warning` | When the screen reader is idle, and only if the alert appears after the page has loaded | Confirmations and updates: a saved draft, a sent application |
| `note` or none | – | Never; it is read in its place on the page | Static guidance that is there from the start: processing times, opening hours |
```tsx
// Static guidance, never announced
…
// An urgent warning, announced at once
…
// No role at all
…
```
#### With Action
`AlertAction` places a button, such as an action or a dismiss button, in the top-right corner.
#### With List
Summarise form errors at the top of a form and link each one to its field.
#### Compact
Title-only or description-only alerts for short, single-line messages.
### Subcomponents
- `Alert` - The container; accepts `variant` (`'default' | 'destructive' | 'info' | 'success' | 'warning'`, default `'default'`), `role` (default `'alert'` for `destructive`, `'status'` otherwise) and all `div` attributes
- `AlertTitle` - The alert title (renders an `h5`)
- `AlertDescription` - The alert description/content
- `AlertAction` - Absolutely positioned slot for an action button in the top-right corner
### Variants
- `default` - Neutral alert for general information
- `destructive` - Alert for errors and critical information
- `info` - Primary tint for information the user should notice
- `success` - Green tint for confirmations, such as a sent application or a granted permit
- `warning` - Amber tint for things that need attention, such as missing documents or a service disruption
The status variants keep body text in `foreground` and colour only the border, background and icon, so text contrast
stays above 4.5:1 in light and dark mode.
### Accessibility
- `destructive` alerts use `role="alert"`, which screen readers announce immediately and interrupt the user for. Keep it for errors and time-sensitive messages
- The other variants use `role="status"`: announced politely when the alert appears after the page has loaded, and silent when it is on the page from the start. This covers WCAG 2.2 success criterion 4.1.3 (Status Messages) without interrupting
- Pass `role="alert"` for an urgent message in another variant, and `role="note"` for static guidance that should never be announced
- Live regions are announced most reliably when the alert is added to a page that is already loaded. For a message shown after navigation, move focus to it or to the page heading instead
- Icons are decorative; make sure the title or description conveys the meaning. Status is carried by text and an icon, never by colour alone
- Give icon-only buttons in `AlertAction` an `aria-label`
### Best Practices
1. **Choose appropriate variants**:
- Use `default` for general notices
- Use `info`, `success` and `warning` for status messages
- Use `destructive` for errors and critical information
2. **Content guidelines**:
- Keep titles concise and descriptive
- Provide clear, actionable descriptions
- Use icons that reinforce the message type
3. **Layout considerations**:
- Place alerts near the content they relate to
- Use consistent spacing between multiple alerts
### Common Patterns
#### Form Validation
```tsx
Fix 2 problems before you send the application
```
#### Success Notification
```tsx
Application sent
Your case number is 2026-04812. We have sent a confirmation to your email.
```
#### Dismissible Banner
```tsx
From 1 June the city library is open until 20:00 on weekdays.
```
## Source
File: components/ui/alert.tsx
```tsx
import { cva, type VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { cn } from '@/lib/utils'
const alertVariants = cva(
"group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-background text-foreground *:[svg]:text-foreground',
destructive:
'border-destructive-border bg-destructive-subtle text-foreground *:[svg]:text-destructive-subtle-foreground',
info: 'border-info-border bg-info-subtle text-foreground *:[svg]:text-info-subtle-foreground',
success: 'border-success-border bg-success-subtle text-foreground *:[svg]:text-success-subtle-foreground',
warning: 'border-warning-border bg-warning-subtle text-foreground *:[svg]:text-warning-subtle-foreground',
},
},
defaultVariants: {
variant: 'default',
},
},
)
/**
* A callout for an inline message.
*
* The default `role` follows the variant: `destructive` is `alert`, which screen readers announce at once and
* interrupt the user for; every other variant is `status`, which is announced politely when the message appears after
* the page has loaded and stays quiet when it is there from the start. Pass `role="alert"` for an urgent message in
* another variant, or `role="note"` (or `role={undefined}`) for static text that should never be announced.
*/
function Alert({ className, variant, ...props }: React.ComponentProps<'div'> & VariantProps) {
return (
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<'h5'>) {
return (
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
className,
)}
{...props}
/>
)
}
function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
return
}
export { Alert, AlertTitle, AlertDescription, AlertAction }
```
---
# Alert Dialog
Modal confirmation dialog that interrupts the user and expects a response.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/alert-dialog.json`
Import from: `@/components/ui/alert-dialog`
Builds on: button
npm dependencies: @base-ui/react@^1.8.0
A modal dialog component that interrupts the user with important content and expects a response.
Built on top of [Base UI Alert Dialog](https://base-ui.com/react/components/alert-dialog),
it follows WAI-ARIA design patterns and provides a fully accessible dialog experience.
### Features
- Focus management
- Keyboard navigation
- Screen reader announcements
- Responsive design
- Customizable content
- Animated transitions
- Blurred backdrop overlay
- Two sizes: `default` and `sm` (`size` on `AlertDialogContent`)
- Optional icon/media slot (`AlertDialogMedia`)
- `AlertDialogAction` and `AlertDialogCancel` render as `Button`, accept its `variant` and `size`, and close the dialog when clicked
- Render the trigger as any component with the `render` prop, e.g. `render={}`
- Mobile-friendly
- ARIA compliant
### Usage
```tsx
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
AlertDialogMedia,
} from '@/components/ui/alert-dialog'
import { Button } from '@/components/ui/button'
import { Trash2Icon } from 'lucide-react'
// Basic usage
export function WithdrawDialog() {
return (
}>Withdraw application
Withdraw case 2026-04812?
The application will be closed and the handling officer notified.
Keep applicationWithdraw
)
}
// Small size with media
export function DeleteDraftDialog() {
return (
}>Delete draft
Delete draft?Your draft application will be deleted permanently.CancelDelete
)
}
```
#### Default
Confirm before a resident sends a school placement application that can’t be changed afterwards.
#### Destructive
A destructive action (`variant="destructive"` on `AlertDialogAction`) for withdrawing a building permit application.
Name the action on the button, not just "OK".
#### Small
A compact dialog (`size="sm"`) with centered content and full-width footer buttons.
#### With Media
An icon in `AlertDialogMedia` sits beside the title on larger screens. Tint it with `className`.
#### Small with Media
At `size="sm"` the media is centered above the title.
#### With Custom Content
Put a summary between the header and the footer so the user can check what they are cancelling.
#### With Form
A required message before a case is returned to the applicant. The action stays disabled until the field is filled in.
### Props
#### AlertDialogContent
- `size` - `'default' | 'sm'` (default `'default'`)
- `className` - Additional CSS classes
#### AlertDialogMedia
- `className` - Additional CSS classes
- `children` - Icon or image shown above (or, at default size on larger screens, beside) the title
#### AlertDialogAction
- `variant` - Button variant (default `'default'`)
- `size` - Button size (default `'default'`)
- Closes the dialog when clicked. For async work, control the dialog with `open` / `onOpenChange` and use a plain `Button` instead.
#### AlertDialogCancel
- `variant` - Button variant (default `'outline'`)
- `size` - Button size (default `'default'`)
### Accessibility
The Alert Dialog component follows WAI-ARIA guidelines for modal dialogs:
- Uses `role="alertdialog"` for critical interactions
- Manages focus trap within dialog
- Supports keyboard navigation:
- `Esc`: Close dialog
- `Tab`: Navigate focusable elements
- `Space/Enter`: Activate buttons
- Announces dialog title and description
- Provides clear action buttons
- Maintains focus on trigger after closing
### Best Practices
1. Content Guidelines:
- Use clear, concise language
- Explain consequences of actions
- Provide obvious choices
- Keep content focused
2. Visual Design:
- Use appropriate spacing
- Maintain clear hierarchy
- Highlight destructive actions
- Ensure readable text
3. Interaction:
- Provide clear feedback
- Allow easy dismissal
- Handle keyboard input
- Consider mobile touch targets
4. Accessibility:
- Write descriptive labels
- Use semantic markup
- Test with screen readers
- Verify keyboard navigation
5. Implementation:
- Handle state changes
- Manage focus correctly
- Consider animations
- Test edge cases
### Related Components
- Dialog
- Sheet
- Popover
- Sheet
## Source
File: components/ui/alert-dialog.tsx
```tsx
'use client'
import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return
}
function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
return
}
function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
return
}
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
return (
)
}
function AlertDialogContent({
className,
size = 'default',
...props
}: AlertDialogPrimitive.Popup.Props & {
size?: 'default' | 'sm'
}) {
return (
)
}
function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AlertDialogMedia({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AlertDialogTitle({ className, ...props }: AlertDialogPrimitive.Title.Props) {
return (
)
}
function AlertDialogDescription({ className, ...props }: AlertDialogPrimitive.Description.Props) {
return (
)
}
function AlertDialogAction({
className,
variant = 'default',
size = 'default',
...props
}: AlertDialogPrimitive.Close.Props & Pick, 'variant' | 'size'>) {
return (
}
{...props}
/>
)
}
function AlertDialogCancel({
className,
variant = 'outline',
size = 'default',
...props
}: AlertDialogPrimitive.Close.Props & Pick, 'variant' | 'size'>) {
return (
}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
}
```
---
# Aspect Ratio
Displays content within a fixed width-to-height ratio.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/aspect-ratio.json`
Import from: `@/components/ui/aspect-ratio`
Displays content within a desired ratio. The Aspect Ratio component uses the CSS `aspect-ratio` property and keeps images, maps, video embeds and document previews at fixed proportions regardless of the available width.
### Features
- Any numeric ratio, e.g. 16 / 9, 4 / 3 or 1
- Scales with its container width
- Works with images, iframes and arbitrary content
- Plain CSS `aspect-ratio`, no primitive dependency
### Usage
```tsx
import { AspectRatio } from '@/components/ui/aspect-ratio'
export function CityHallImage() {
return (
)
}
```
`ratio` defaults to `1`. The component is a single `div` with `position: relative` and the CSS `aspect-ratio`, so
`className` styles the ratio box itself and children can be placed with `absolute` too.
#### Default
A 16:9 image slot with a caption. The examples use token-based placeholders where your app would show a photo.
#### Square
1:1 thumbnails for a grid of municipal facilities.
#### Portrait
An A4 ratio (`1 / Math.SQRT2`) for a preview of a scanned or generated document.
#### Map Embed
A 4:3 container for an embedded property map, with overlays positioned on top.
#### Video
A 16:9 frame for a recorded council meeting, with a play button and duration.
### Accessibility
- Purely presentational: it does not add any roles or semantics
- Always provide meaningful alt text for images placed inside
- Give embedded iframes a descriptive title attribute
## Source
File: components/ui/aspect-ratio.tsx
```tsx
import type * as React from 'react'
import { cn } from '@/lib/utils'
function AspectRatio({ ratio = 1, className, style, ...props }: React.ComponentProps<'div'> & { ratio?: number }) {
return (
)
}
export { AspectRatio }
```
---
# Attachment
File attachment chip with name, size and remove action, for chat and forms.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/attachment.json`
Import from: `@/components/ui/attachment`
Builds on: button
npm dependencies: @base-ui/react@^1.8.0, class-variance-authority@^0.7.1
Displays a file attached to a message, form or case, with media, title, description and actions. It communicates upload state (idle, uploading, processing, error and done), comes in three sizes and two orientations, and can be grouped into a horizontally scrolling row.
### Features
- Five states: idle, uploading, processing, error and done
- Sizes: default, sm and xs
- Horizontal and vertical orientation
- Icon or image media
- Inline actions such as remove, retry or download
- AttachmentTrigger makes the whole attachment clickable, e.g. to open a preview dialog
- AttachmentGroup for a scroll-snapping row of attachments
### Usage
```tsx
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from '@/components/ui/attachment'
import { FileText, X } from 'lucide-react'
export function ApplicationAttachment() {
return (
building-permit-application.pdfPDF · 2.4 MB
)
}
```
#### Default
A completed attachment with a remove action.
#### States
All five upload states, from an idle drop target to a finished file.
#### Sizes
The default, sm and xs sizes.
#### Vertical
Vertical orientation for grid or gallery layouts.
#### Image Media
Thumbnails via `variant="image"` on `AttachmentMedia`. Put an `img` inside; the example uses a token-based
placeholder instead of a photo.
#### Group
`AttachmentGroup` lays attachments out in a scroll-snapping row, here inside a card where a resident sends a supplement
to a building permit case.
#### With Trigger
The whole attachment opens a preview dialog through AttachmentTrigger.
### Accessibility
- Give every icon-only AttachmentAction an aria-label that names the file
- AttachmentTrigger renders a button covering the attachment; give it an aria-label describing the action
- Actions are layered above the trigger so they remain individually focusable
- Error state is conveyed by colour and by the description text, not colour alone
## Source
File: components/ui/attachment.tsx
```tsx
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { cva, type VariantProps } from 'class-variance-authority'
import type * as React from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
const attachmentVariants = cva(
'group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-2 focus-within:ring-ring has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive-border data-[state=idle]:border-dashed',
{
variants: {
size: {
default:
'gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2',
sm: 'gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5',
xs: 'gap-1.5 rounded-lg text-xs has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1',
},
orientation: {
horizontal: 'min-w-40 items-center',
vertical: 'w-24 flex-col has-data-[slot=attachment-content]:w-30',
},
},
},
)
function Attachment({
className,
state = 'done',
size = 'default',
orientation = 'horizontal',
...props
}: React.ComponentProps<'div'> &
VariantProps & {
state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done'
}) {
return (
)
}
const attachmentMediaVariants = cva(
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive-subtle group-data-[state=error]/attachment:text-destructive-subtle-foreground group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
{
variants: {
variant: {
icon: '',
image:
'opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover',
},
},
defaultVariants: {
variant: 'icon',
},
},
)
function AttachmentMedia({
className,
variant = 'icon',
...props
}: React.ComponentProps<'div'> & VariantProps) {
return (
)
}
function AttachmentContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AttachmentTitle({ className, ...props }: React.ComponentProps<'span'>) {
return (
)
}
function AttachmentDescription({ className, ...props }: React.ComponentProps<'span'>) {
return (
)
}
function AttachmentActions({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AttachmentAction({ className, variant, size = 'icon-xs', ...props }: React.ComponentProps) {
return (
)
}
function AttachmentTrigger({ className, render, type, ...props }: useRender.ComponentProps<'button'>) {
return useRender({
defaultTagName: 'button',
props: mergeProps<'button'>(
{
type: render ? type : (type ?? 'button'),
className: cn('absolute inset-0 z-10 outline-none', className),
},
props,
),
render,
state: {
slot: 'attachment-trigger',
},
})
}
function AttachmentGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
export {
Attachment,
AttachmentGroup,
AttachmentMedia,
AttachmentContent,
AttachmentTitle,
AttachmentDescription,
AttachmentActions,
AttachmentAction,
AttachmentTrigger,
}
```
---
# Avatar
User image with a text fallback, built on Base UI Avatar.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/avatar.json`
Import from: `@/components/ui/avatar`
npm dependencies: @base-ui/react@^1.8.0
A versatile avatar component for displaying user images with graceful fallbacks.
Built on top of [Base UI Avatar](https://base-ui.com/react/components/avatar),
it provides a fully accessible and customizable avatar implementation.
### Features
- Image-based avatars with automatic fallback while loading or on error
- Three sizes via `size`: `sm` (24px), `default` (32px), `lg` (40px)
- `AvatarBadge` for status dots or small icons in the bottom-right corner
- `AvatarGroup` for overlapping stacks, with `AvatarGroupCount` for overflow
- Group count adapts to the size of the avatars in the group
- Custom styling via `className`
### Usage
```tsx
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from '@/components/ui/avatar'
// Basic usage
export function OfficerAvatar() {
return (
AL
)
}
// Large with status badge
export function StatusAvatar() {
return (
IH
)
}
// Group
export function CaseTeam() {
return (
ALOH+3
)
}
```
#### Default
The handling officer on a case, shown with initials next to their name and role.
#### With Fallback
When the image is missing or fails to load, `AvatarFallback` shows the person’s initials instead.
#### Sizes
Avatars come in `sm`, `default` and `lg`. For other sizes, override with a `size-*` class.
#### With Badge
`AvatarBadge` renders a status dot; colour it with a theme token via `className`. Icons inside the badge are shown on
`default` and `lg` avatars. Always pair the dot with a text status so the meaning doesn’t rely on colour alone.
#### Avatar Group
`AvatarGroup` overlaps avatars with a ring, and `AvatarGroupCount` shows the remaining count or an icon.
#### Custom Styles
A filled fallback, a ring marking the signed-in user, an icon for an organisation, and a custom `size-14`.
### Subcomponents
- `Avatar` - Root; `size?: 'sm' | 'default' | 'lg'` (default `'default'`)
- `AvatarImage` - The image; hidden until it has loaded
- `AvatarFallback` - Shown while the image loads or when it fails; `delay` (ms) postpones it to avoid a flash on fast connections
- `AvatarBadge` - Status indicator placed inside `Avatar`
- `AvatarGroup` - Overlapping container for multiple avatars
- `AvatarGroupCount` - Overflow indicator placed last in `AvatarGroup`
### Accessibility
- Provide meaningful `alt` text on `AvatarImage`
- Status conveyed by `AvatarBadge` color should also be available as text (e.g. a tooltip or visually hidden label)
- Keep fallback text short and readable
### Best Practices
1. Image Usage:
- Use appropriately sized images
- Provide meaningful alt text
- Handle image errors gracefully with a fallback
2. Fallback Content:
- Keep text short (1-2 characters)
- Ensure readable contrast
3. Sizing:
- Use the built-in sizes for consistency
- Keep a consistent size within a group
4. Grouping:
- Limit visible avatars and use `AvatarGroupCount` for the rest
### Related Components
- Badge
- Tooltip
## Source
File: components/ui/avatar.tsx
```tsx
'use client'
import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar'
import type * as React from 'react'
import { cn } from '@/lib/utils'
function Avatar({
className,
size = 'default',
...props
}: AvatarPrimitive.Root.Props & {
size?: 'default' | 'sm' | 'lg'
}) {
return (
)
}
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
return (
)
}
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
return (
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return (
svg]:hidden',
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
className,
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
return (
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
className,
)}
{...props}
/>
)
}
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge }
```
---
# Badge
Small status or count label with semantic color variants.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/badge.json`
Import from: `@/components/ui/badge`
npm dependencies: @base-ui/react@^1.8.0, class-variance-authority@^0.7.1
A versatile badge component for displaying short status descriptors, labels, or counts.
Built with class-variance-authority for flexible styling and variants, it provides a consistent way to show metadata or status.
### Features
- Nine visual variants: `default`, `secondary`, `destructive`, `info`, `success`, `warning`, `outline`, `ghost`, `link`
- Single compact size (20px high, pill shaped)
- Icon support with `data-icon="inline-start"` / `data-icon="inline-end"` (icons are sized to 12px automatically)
- Polymorphic rendering with the `render` prop (e.g. render as a link, with hover styles)
- Exposes `data-slot="badge"` and `data-variant` for styling hooks
- Focus ring and `aria-invalid` styling
### Usage
```tsx
import { Badge } from '@/components/ui/badge'
import { BadgeCheck } from 'lucide-react'
// Basic usage
export function StatusBadge() {
return Draft
}
// With variants
export function VariantBadges() {
return (
DraftSubmittedRejectedIn reviewApprovedAwaiting documentsArchivedPublic record
)
}
// With icon
export function SignedBadge() {
return (
Signed with BankID
)
}
// As link
export function CaseLink() {
return (
} variant="link">
2026-04812
)
}
```
#### Default
#### All Variants
Each variant with the kind of label it suits.
#### Case Status
Status badges in a case list: `info` for a case in progress, `warning` for one waiting on the applicant, `success` for
an approval and `destructive` for a rejection. Each status also has an icon and text, so it never relies on colour
alone.
```tsx
Awaiting documents
```
#### With Icons
Mark icons with `data-icon` so the badge tightens its padding on that side.
#### Counts
Use `min-w-5 px-1 tabular-nums` for numeric counters, here in a case inbox navigation.
#### As Link
With the `render` prop, the badge renders as another element, such as a button that removes a filter. Variants include
hover styles for anchors.
### Props
- `variant` - Visual style variant (`'default' | 'secondary' | 'destructive' | 'info' | 'success' | 'warning' | 'outline' | 'ghost' | 'link'`, default `'default'`)
- `render` - The element to render instead of a `span`, e.g. `render={}`
- `className` - Additional CSS classes
- All HTML `span` attributes
`badgeVariants` is also exported for applying badge styles to other elements.
### Variants
- `default` - Muted background with foreground text
- `secondary` - Muted, lower emphasis style
- `destructive` - Soft tinted style for errors or failures
- `info` - Soft primary tint for neutral progress, such as a case in review
- `success` - Soft green tint for completed or approved states
- `warning` - Soft amber tint for states that need attention, such as missing documents
- `outline` - Bordered style with transparent background
- `ghost` - No background until hovered
- `link` - Underlined text link style
### Accessibility
- Renders a `span` by default, so it does not add semantics
- Status is also carried by the text, and ideally an icon, never by colour alone: `info`, `success`, `warning` and `destructive` add tone, the label says what it means
- The status variants keep at least 4.5:1 text contrast on their tint in light and dark mode, in both themes
- When rendering as a link with `render`, the badge keeps the anchor semantics and focus ring
### Best Practices
1. Content Guidelines:
- Keep text concise
- Use clear language
- Consider localization
2. Visual Design:
- Use `destructive` only for errors
- Use `success`, `warning` and `info` for status, and keep one meaning per variant across an app
- Prefer `secondary` or `outline` for neutral metadata
3. Icon Usage:
- Use relevant icons
- Mark them with `data-icon` for correct spacing
### Related Components
- Button
- Label
## Source
File: components/ui/badge.tsx
```tsx
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
{
variants: {
variant: {
default: 'bg-muted text-foreground [a]:hover:bg-muted/80',
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
destructive: 'bg-destructive-subtle text-destructive-subtle-foreground [a]:hover:bg-destructive-subtle/60',
info: 'bg-info-subtle text-info-subtle-foreground [a]:hover:bg-info-subtle/60',
success: 'bg-success-subtle text-success-subtle-foreground [a]:hover:bg-success-subtle/60',
warning: 'bg-warning-subtle text-warning-subtle-foreground [a]:hover:bg-warning-subtle/60',
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
link: 'text-primary underline underline-offset-4',
},
},
defaultVariants: {
variant: 'default',
},
},
)
function Badge({
className,
variant = 'default',
render,
...props
}: useRender.ComponentProps<'span'> & VariantProps) {
return useRender({
defaultTagName: 'span',
props: mergeProps<'span'>(
{
className: cn(badgeVariants({ variant }), className),
},
props,
),
render,
state: {
slot: 'badge',
variant,
},
})
}
export { Badge, badgeVariants }
```
---
# Banner
Page-wide message at the top of a page, such as planned maintenance or an outage, that can be dismissed.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/banner.json`
Import from: `@/components/ui/banner`
Builds on: button
npm dependencies: @base-ui/react@^1.8.0, class-variance-authority@^0.7.1, lucide-react@^1.48.0
A page-wide message at the top of the page, such as planned maintenance, longer handling times or an outage. For a
message about one part of the page, such as a form, use Alert instead.
### Features
- Four variants: `info` (default), `warning`, `destructive` and `success`, each with a tinted background and its own icon
- The text keeps the normal text colour, so it stays readable in every variant, theme and colour mode
- The variant is also given by the icon and a screen reader prefix ("Warning: "), not by colour alone
- Spans the full width, with the content centred in a max-width container. It wraps on small screens
- Composable `BannerTitle`, `BannerDescription` and `BannerAction` (a link, or a button with `render`)
- Optional dismiss button, controlled (`open` and `onOpenChange`) or uncontrolled (`defaultOpen`)
- `role="status"` by default, `role="alert"` for urgent messages. It never moves focus
- All user-facing text can be overridden: `label` for the screen reader prefix and `closeLabel` for the dismiss button
### Usage
```tsx
import { Banner, BannerAction, BannerDescription, BannerTitle } from '@/components/ui/banner'
import { WrenchIcon } from 'lucide-react'
export function MaintenanceBanner() {
return (
} onDismiss={() => {}}>
Planned maintenance on Saturday
E-services are unavailable on Saturday 16 May 2026 from 22:00 to 02:00.
Read more
)
}
```
Place the banner first on the page, above the site header, so it is the first thing both sighted and screen reader users
meet.
#### Default
Planned maintenance with a link to more information. `icon` replaces the variant's icon.
#### Variants
`info` for news and planned events, `warning` for things that may affect the user, `destructive` for outages and
errors, and `success` when a problem is solved. The outage uses `role="alert"`.
#### Dismissible
Pass `onDismiss` or `onOpenChange` to show the dismiss button. Control it with `open` to show it again, or to remember
the choice. The banner leaves the page when it is dismissed, so move focus to a sensible place in `onDismiss`.
#### With Action
`BannerAction` renders a link by default. Use `render={}` for an action that isn't navigation.
`closeLabel` sets the name of the dismiss button.
#### Page Mockup
An urgent outage above the site header.
### Props
| Prop | Type | Default | Description |
| -------------- | -------------------------------------------------- | --------------- | ---------------------------------------------------------------------- |
| `variant` | `'info' \| 'warning' \| 'destructive' \| 'success'` | `'info'` | The kind of message |
| `role` | `'status' \| 'alert'` | `'status'` | `alert` only for urgent messages |
| `icon` | `ReactNode \| false` | variant's icon | Replace the icon, or `false` to hide it |
| `label` | `string \| false` | variant's label | Screen reader prefix: "Information", "Warning", "Error" or "Success" |
| `open` | `boolean` | | Controlled visibility |
| `defaultOpen` | `boolean` | `true` | Initial visibility when uncontrolled |
| `onOpenChange` | `(open: boolean) => void` | | Called when the banner is dismissed |
| `onDismiss` | `() => void` | | Called when the banner is dismissed |
| `dismissible` | `boolean` | see description | Show the dismiss button. `true` when `onDismiss` or `onOpenChange` is set |
| `closeLabel` | `string` | `'Dismiss'` | Accessible name of the dismiss button |
Set `--banner-max-width` with `className` to match the width of your page layout, for example
`[--banner-max-width:var(--container-5xl)]`. The default is `--container-7xl` (80rem).
### Subcomponents
- `Banner`: the full-width container with the icon, the content and the dismiss button
- `BannerTitle`: the short headline, in medium weight
- `BannerDescription`: the message. Links in it are underlined
- `BannerAction`: a link (default) or a button with `render`, such as "Read more"
### Accessibility
- `role="status"` is polite: screen readers read the message when it appears after the page has loaded, without
interrupting. Use `role="alert"` only for urgent messages, such as an outage that stops the user's task
- A banner that is part of the page from the start is not announced as a live region, so place it first in the page
where it is read before the header
- The banner never moves focus when it appears
- The variant is conveyed by the icon and a visually hidden prefix such as "Warning: ", not by colour alone. Translate
the prefix with `label`, or pass `label={false}` when the title already says what kind of message it is
- The text uses the normal text colour on a light tint, so it meets the 4.5:1 contrast minimum
- The dismiss button is 32×32px and named by `closeLabel`. When the banner is dismissed it leaves the page, so move focus
to a sensible place in `onDismiss`, for example the main heading
- Links in the description and `BannerAction` are underlined and have a visible focus ring
### Related Components
- Alert: messages about one part of the page, such as a form
- Button
## Source
File: components/ui/banner.tsx
```tsx
'use client'
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { cva, type VariantProps } from 'class-variance-authority'
import { CircleAlertIcon, CircleCheckIcon, InfoIcon, TriangleAlertIcon, XIcon } from 'lucide-react'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
// Each variant has its own tint and icon colour. The variant is also carried by the icon and the screen reader prefix,
// never by colour alone.
const bannerVariants = cva(
'group/banner w-full border-b text-sm text-foreground [--banner-max-width:var(--container-7xl)]',
{
variants: {
variant: {
info: 'border-info-border bg-info-subtle [&_[data-slot=banner-icon]]:text-info-subtle-foreground',
warning: 'border-warning-border bg-warning-subtle [&_[data-slot=banner-icon]]:text-warning-subtle-foreground',
destructive:
'border-destructive-border bg-destructive-subtle [&_[data-slot=banner-icon]]:text-destructive-subtle-foreground',
success: 'border-success-border bg-success-subtle [&_[data-slot=banner-icon]]:text-success-subtle-foreground',
},
},
defaultVariants: {
variant: 'info',
},
},
)
type BannerVariant = NonNullable['variant']>
const defaultIcons: Record = {
info: InfoIcon,
warning: TriangleAlertIcon,
destructive: CircleAlertIcon,
success: CircleCheckIcon,
}
const defaultLabels: Record = {
info: 'Information',
warning: 'Warning',
destructive: 'Error',
success: 'Success',
}
function Banner({
className,
variant = 'info',
role = 'status',
icon,
label,
open: openProp,
defaultOpen = true,
onOpenChange,
onDismiss,
dismissible,
closeLabel = 'Dismiss',
children,
...props
}: Omit, 'role'> &
VariantProps & {
/**
* `status` (default) is announced politely when the message appears after the page has loaded. Use `alert` only for
* urgent messages, such as an ongoing outage; screen readers interrupt the user to read it.
*/
role?: 'status' | 'alert'
/** Replace the variant's icon, or pass `false` to hide it */
icon?: React.ReactNode
/**
* Screen reader prefix that names the kind of message, such as "Warning". Defaults to an English label for the
* variant; pass a translation, or `false` when the title already says it.
*/
label?: string | false
/** Controlled visibility */
open?: boolean
/** Initial visibility when uncontrolled */
defaultOpen?: boolean
/** Called with the new visibility when the banner is dismissed */
onOpenChange?: (open: boolean) => void
/** Called when the user dismisses the banner. Passing it shows the dismiss button */
onDismiss?: () => void
/** Show the dismiss button. Defaults to `true` when `onDismiss` or `onOpenChange` is passed */
dismissible?: boolean
/** Accessible name of the dismiss button */
closeLabel?: string
}) {
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
const open = openProp ?? uncontrolledOpen
const showDismiss = dismissible ?? (onDismiss !== undefined || onOpenChange !== undefined)
const resolvedVariant: BannerVariant = variant ?? 'info'
if (!open) return null
const Icon = defaultIcons[resolvedVariant]
const prefix = label === false ? null : (label ?? defaultLabels[resolvedVariant])
function dismiss() {
if (openProp === undefined) setUncontrolledOpen(false)
onOpenChange?.(false)
onDismiss?.()
}
return (
{icon === false ? null : (
{icon ?? }
)}
{prefix ? {`${prefix}: `} : null}
{children}
{showDismiss ? (
) : null}
)
}
function BannerTitle({ className, ...props }: React.ComponentProps<'p'>) {
return
}
function BannerDescription({ className, ...props }: React.ComponentProps<'p'>) {
return (
)
}
// Renders a link by default. Use `render={}` for an action that is not navigation.
function BannerAction({ className, render, ...props }: useRender.ComponentProps<'a'>) {
return useRender({
defaultTagName: 'a',
props: mergeProps<'a'>(
{
className: cn(
'inline-flex min-h-6 cursor-pointer items-center gap-1 rounded-sm font-medium whitespace-nowrap underline underline-offset-4 outline-none hover:decoration-2 focus-visible:ring-3 focus-visible:ring-ring [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-4',
className,
),
},
props,
),
render,
state: {
slot: 'banner-action',
},
})
}
export { Banner, BannerTitle, BannerDescription, BannerAction, bannerVariants }
```
---
# Breadcrumb
Hierarchical page trail with separators and an overflow ellipsis.
Install: `npx shadcn@latest add https://rdf.dev.360online.com/r/breadcrumb.json`
Import from: `@/components/ui/breadcrumb`
npm dependencies: @base-ui/react@^1.8.0, lucide-react@^1.48.0
A breadcrumb navigation component that helps users understand their current location within a hierarchical structure.
### Features
- Semantic HTML structure using `