rCTF Docs
Overview

Components

Overview of the rCTF UI component library based on shadcn-svelte.

The rCTF UI layer is built on shadcn-svelte, a port of shadcn/ui for Svelte 5. Components live in apps/web/src/lib/components/ui/ and use tailwind-variants for variant-based styling. Application-specific components (Navigation, Markdown, ThemeToggle) are in the parent components/ directory. Icons come from Tabler Icons via @iconify-svelte.

The following components are available in rCTF:

CategoryComponents
PrimitivesButton, Badge, Input, Textarea, Checkbox, Label, Separator, Progress, Spinner, Skeleton
LayoutCard, Section, ScrollArea, Resizable, Sidebar
OverlaysDialog, Drawer, Sheet, Tooltip, Popover, DropdownMenu
DataTable, Accordion, Tabs, VirtualList*, Chart
FormsField, InputGroup, Select, TagInput*, SchemaForm*
NavigationCommand, Pagination, ButtonGroup
AppNavigation*, NavigationMobile*, ThemeToggle*, Markdown*, FlagPicker*, EmptyState*, SearchInput*

* These components are custom to rCTF and are not part of shadcn-svelte.

Adding components#

When adding a new shadcn-svelte component to rCTF, you need to install it, register it in the barrel file, and convert its styling to match rCTF’s design system. This section walks through the full process using Button as an example.

Install the component

Use the shadcn-svelte CLI to scaffold the component into your project:

Terminal window
bunx shadcn-svelte@latest add button

This creates the component files in apps/web/src/lib/components/ui/button/.

Register in the barrel file

Add the component to the barrel export in apps/web/src/lib/components/index.ts:

lib/components/index.ts
export { Badge, badgeVariants, type BadgeVariant } from './ui/badge'
export { default as EmptyState } from './empty-state.svelte'
export {
Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
} from './ui/button'
export { Checkbox } from './ui/checkbox'
export { Input } from './ui/input'
export { Label } from './ui/label'
Remove shadows

rCTF uses a flat design language. Remove all shadow utilities from the component:

button.svelte
export const buttonVariants = tv({
base: '... shadow-xs',
base: '...',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive hover:bg-destructive/90 ... shadow-xs',
destructive: 'bg-destructive hover:bg-destructive/90 ...',
outline: 'bg-background hover:bg-accent ... shadow-xs',
outline: 'bg-background hover:bg-accent ...',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
},
},
})
Normalize font weights

Convert all heavy font weights to font-medium for visual consistency (font-semibold, font-bold, font-extrabold, font-black).

Convert color tokens

Replace shadcn’s color tokens with rCTF’s layered color system. The mapping depends on context, so use these as guidelines rather than strict rules:

rCTF tokenExampleLightDarkContext
--backgroundbg-background-l0Page background
--foregroundtext-foreground-l0Primary body text
--cardbg-background-l1Card surfaces
--card-foregroundtext-foreground-l0Card text
--popoverbg-background-l1Dropdowns, popovers
--popover-foregroundtext-foreground-l0Text in overlays
--primarybg-background-accentAccent buttons, badges
--primary-foregroundtext-foreground-accentText on accent backgrounds/accent-colored text
--secondarybg-background-l4Secondary buttons, badges
--secondary-foregroundtext-foreground-l1Text on secondary backgrounds
--mutedbg-background-l2Subtle backgrounds
--muted-foregroundtext-foreground-l3Placeholders, hints
--accentbg-background-l2Hover states, highlights, generic hover
--accent-foregroundtext-foreground-l0Text on hover backgrounds
--destructivebg-background-destructiveError states
--inputbg-background-l4Form inputs
hover:bg-background-accent-hoverhoverAccent hover states
hover:bg-background-l5hoverSecondary hover states

Here’s an example converting the Badge component variants:

badge.svelte
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
default: "bg-background-accent text-foreground-accent [a&]:hover:bg-background-accent/90 border-transparent",
secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
secondary: "bg-background-l4 text-foreground-l1 [a&]:hover:bg-background-l5 border-transparent",
destructive: "bg-destructive [a&]:hover:bg-destructive/90 ... dark:bg-destructive/70 border-transparent text-white",
destructive: "bg-background-destructive text-foreground-destructive [a&]:hover:bg-background-destructive/90 ... border-transparent",
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
outline: "text-foreground-l1 [a&]:hover:bg-background-l2",
},
},
Remove dark mode overrides (optional)

rCTF’s color system handles dark mode automatically via CSS custom properties. Remove all dark: prefixed utilities.

Remove transitions (optional)

rCTF components generally don’t use transitions to avoid grogginess/exhaustion when repeatedly using the same interfaces. Remove if desired.

Full example#

Here’s the complete before and after for the Button component:

<script lang="ts" module>
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&\_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
destructive:
'bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs',
outline:
'bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
})
// ...
</script>
<script lang="ts" module>
import { cn, type WithElementRef } from '$lib/utils'
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'
import { tv, type VariantProps } from 'tailwind-variants'
export const buttonVariants = tv({
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-foreground-destructive/20 aria-invalid:border-foreground-destructive inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&\_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-background-accent text-foreground-accent hover:bg-background-accent-hover',
destructive:
'bg-background-destructive text-foreground-destructive hover:bg-background-destructive-hover focus-visible:ring-foreground-destructive/20 border-foreground-destructive/50',
outline: 'bg-background-l1 hover:bg-background-l2 text-foreground-l1 border',
secondary: 'bg-background-l4 text-foreground-l1 hover:bg-background-l5',
ghost: 'hover:bg-background-l3 text-foreground-l1',
link: 'text-foreground-prose-link underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
})
// ...
</script>
Esc

Start typing to search the docs.