rCTF builds its light and dark themes from Radix Colors. Each challenge category has its own icon and hue. The interface uses Svelte components with scoped CSS, while Zag.js handles state for interactive controls such as dialogs and menus.
rCTF builds its palette from Radix Colors. The values and theme tokens are defined in apps/web/src/styles/color.css.
Each token defines its light and dark values with light-dark(). Set data-theme="light" or data-theme="dark" on <html> to choose one. Without that attribute, prefers-color-scheme follows the operating system setting.
The theme toggle saves the choice to localStorage. On later visits, static/theme.js applies it before the page paints so the other theme does not flash during loading.
Color reference
Layered colors
Most of the interface uses Radix’s neutral gray scale, arranged into six background and foreground layers named l0 through l5. Backgrounds move from the page-level gray-1 toward gray-7, becoming more prominent as the layer number rises. Foregrounds move the other way, starting with gray-12 for primary text and fading toward gray-8.
Layer
Background (light)
Background (dark)
Foreground (light)
Foreground (dark)
l0
gray-1
gray-1
gray-12
gray-12
l1
gray-3
gray-2
gray-11
gray-11
l2
gray-4
gray-3
gray-10
gray-10
l3
gray-5
gray-4
gray-9
gray-9
l4
gray-6
gray-5
gray-8
gray-8
l5
gray-7
gray-6
gray-8
gray-7
Component CSS can use these tokens wherever it needs a layered color, for example background: var(--background-l1) or color: var(--foreground-l2).
Semantic colors
Semantic colors convey meaning. Each role pairs a soft background wash with a readable foreground, and the interactive roles add a -hover background variant.
These colors appear on scoreboards, podiums, and blood medals. gold, silver, and bronze mark the top three places, self highlights the current user’s row, and nth uses the neutral layers for everyone else.
Color
Background (light)
Background (dark)
Foreground (light)
Foreground (dark)
gold
amber-a3
amber-9 15%
amber-11
amber-11
silver
slate-a3
slate-9 30%
slate-11
slate-11
bronze
brown-a5
brown-9 25%
brown-11
brown-11
self
jade-3
jade-3
jade-11
jade-11
nth
background-l4
background-l3
foreground-l0
foreground-l3
Use the -l0 foregrounds for primary text and -l1 for secondary text.
Graph colors
Graph colors are only used for lines on graphs and sparklines. The tokens run from --foreground-first to --foreground-tenth. Each one is step 9 of a different Radix hue and stays the same in both modes.
Color
Light
Dark
first
red-9
red-9
second
amber-9
amber-9
third
lime-9
lime-9
fourth
jade-9
jade-9
fifth
cyan-9
cyan-9
sixth
sky-9
sky-9
seventh
blue-9
blue-9
eighth
violet-9
violet-9
ninth
plum-9
plum-9
tenth
pink-9
pink-9
Prose colors
Prose colors style rendered Markdown, such as challenge descriptions and homepage content.
Color
Light
Dark
prose
gray-11
gray-11
prose-link
sky-11
sky-11
prose-inline-code
gray-11
red-11
Category colors
Challenge categories pick from ten hues. Each hue has two background tints plus a hover variant and two foreground levels. All of them come from Radix’s alpha scales, so they blend into whatever layer they sit on.
Color
--background-<hue>-l0
--background-<hue>-l1
--foreground-<hue>-l0
--foreground-<hue>-l1
crimson
crimson-a3
crimson-a2
crimson-a12
crimson-a11
red
red-a3
red-a2
red-a12
red-a11
orange
orange-a3
orange-a2
orange-a12
orange-a11
yellow
yellow-a3
yellow-a2
yellow-a12
yellow-a11
green
green-a3
green-a2
green-a12
green-a11
teal
teal-a3
teal-a2
teal-a12
teal-a11
sky
sky-a3
sky-a2
sky-a12
sky-a11
violet
violet-a3
violet-a2
violet-a12
violet-a11
plum
plum-a3
plum-a2
plum-a12
plum-a11
gray
gray-a3
gray-a2
gray-a12
gray-a11
A [data-category-color='<hue>'] block in color.css points the generic --category-* tokens at one of these ramps. See Categories for how a component picks its hue.
Categories
Challenge category configuration, colors, icons, and customization.
Add your category to categoryOrder to control its position in the challenge list:
lib/utils/categories.ts
1
exportconstcategoryOrder= [
2
'koth',
3
'ad',
4
'sanity',
5
'pwn',
6
'reverse',
7
'crypto',
8
'forensics',
9
'blockchain',
10
'web',
11
'misc',
12
'ppc',
13
'osint',
14
'hardware',
15
]
Categories not in this list show up alphabetically after the listed ones. Add the same key to scoreboardCategoryOrder if it should have a fixed scoreboard column.
Add a hue (if using a new color)
Skip this step when reusing an existing hue. For a new one, add it to CategoryColor in categories.ts and define its five variables in apps/web/src/styles/color.css. Then add a [data-category-color='<hue>'] selector that maps the generic --category-* tokens to the new values.
Utility functions
Function
Purpose
getCategoryConfig(category)
Returns { name, icon, color } for a category key. Unknown keys fall back to a flag icon, gray, and the key itself as the display name
getCategoryKeyOrAlias(category)
Lowercases the key and resolves aliases to canonical keys ('rev' => 'reverse')
getCategoryOrder(category)
Returns the sort index in categoryOrder (-1 if unlisted)
getScoreboardCategoryOrder(category)
Returns the sort index in scoreboardCategoryOrder (-1 if unlisted)
Components
How rCTF builds UI components from Zag.js machines and scoped CSS.
rCTF’s interface is built from Svelte 5 components with scoped <style> blocks. Interactive widgets such as dialogs and menus use Zag.js state machines for their behavior, while the surrounding markup and styles remain in the Svelte components.
The interface is split across these directories.
Path
Contents
apps/web/src/lib/ui/
Reusable controls and layout primitives
apps/web/src/lib/components/
Features such as the navigation bar and Markdown renderer
apps/web/src/lib/icons/
Local Svelte icon components, mostly sourced from Phosphor Icons
Variants are represented with data attributes, while layout components use custom tags such as <ui-card>.
Components are grouped by how they handle behavior.
When adding a component, choose the simplest tier that fits its behavior and keep its variants and state in data attributes. Most components only need a single Svelte file.
Create the component file
Add a kebab-case.svelte file under apps/web/src/lib/ui/. Give it a single typeProps= {...}, destructured with defaults. Boolean attributes are forwarded as attr={value ||undefined} so they only appear when truthy.
Pick a tier
If HTML and CSS cover it, use a semantic element or a custom-element tag and a scoped <style> block (do not use JavaScript). If it has interactive state (open/closed, selection, focus management), drive it with a @zag-js/* machine wrapped once in the component.
Express variants as data attributes
Never branch styling on classes. Emit data-variant, data-size, data-state, and target them in the scoped style (&[data-variant='destructive']). Zag parts already carry data-part and data-state, so style those directly.
Use theme tokens
Reference theme tokens from the component CSS, using values such as var(--background-l1), var(--foreground-accent), and var(--radius-md).
Wire Zag correctly (machines only)
The reactive props passed to useMachinemust be a thunk, or controlled state silently freezes:
constservice=useMachine(dialog.machine, () => ({ id, open }))
For a native component with variants, see button.svelte. Depending on whether href is set, it renders either a <button> or an <a> and styles it through the data-variant and data-size attributes. The default variant uses var(--background-accent), for example, while the small size reduces the component’s height.