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:
* 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.
Use the shadcn-svelte CLI to scaffold the component into your project:
bunx shadcn-svelte@latest add buttonThis creates the component files in apps/web/src/lib/components/ui/button/.
Add the component to the barrel export in apps/web/src/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'rCTF uses a flat design language. Remove all shadow utilities from the component:
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', }, },})Convert all heavy font weights to font-medium for visual consistency (font-semibold, font-bold, font-extrabold, font-black).
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:
Here’s an example converting the Badge component variants:
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", },},rCTF’s color system handles dark mode automatically via CSS custom properties. Remove all dark: prefixed utilities.
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>