| name | new-section-component |
| description | End-to-end workflow for creating a new section component: canvas design first, then codebase implementation following 4Geeks architecture and design system. |
New Section Component Skill
This skill defines the two-phase workflow for creating any new section component in the 4Geeks marketing platform. Every new component must be designed visually on the canvas first, approved by the user, and then built into the codebase following the architecture and design system documented below.
Phase 1: Canvas Design (Mockup Sandbox)
Before writing any production code, prototype the component visually in a mockup sandbox so the user can review and approve the design.
Steps
- Clarify requirements โ Ask the user what the section should accomplish, what content it displays, and any layout preferences (e.g., number of columns, card vs. flat, image placement).
- Create a mockup sandbox artifact โ Use
createArtifact() with artifactType: "mockup-sandbox". Build a self-contained HTML/CSS/JS prototype that:
- Uses the 4Geeks design tokens (colors, typography, spacing, radius) listed in the Design System Reference below.
- Shows realistic placeholder content (never lorem ipsum โ use marketing-appropriate copy).
- Demonstrates responsive behavior (desktop and mobile).
- If the component has variants, show each variant.
- Present to the user โ Let the user review in the preview pane. Iterate on feedback until they approve.
- Lock the design โ Once approved, document the final layout decisions (variant names, props, content structure) before moving to Phase 2.
Phase 2: Codebase Implementation
After the user approves the mockup, build the component into the codebase. Every new section component requires all of the following files and registrations.
File Checklist
1. Component Registry โ 4geeks-com/component-registry/<component_name>/v1.0/
Create the following files inside a new versioned folder:
| File | Purpose |
|---|
schema.yml | Human-readable component metadata: name, version, component name, file path, description, when_to_use, variants (if any), props with types/defaults/descriptions, and optional section_defaults. |
schema.ts | Zod validation schemas and TypeScript types. Export the unified section schema (use z.union for multi-variant components) plus all sub-schemas and types. |
field-editors.ts | Optional. Maps prop names to custom inline-editor types (e.g., "font-size-picker"). Export fieldEditors: Record<string, EditorType>. |
examples/ | One or more .yml files with realistic YAML examples. Each file has name, description, and yaml (a YAML string showing the section in a sections array). Every variant must have at least one example. Additionally, if a single prop can drastically change the component's approach or objective โ such as switching the layout structure, surfacing a completely different metric, or toggling an entire sub-UI like a form โ that warrants its own separate example file. Minor content differences (different background color, different copy) do not qualify. Rule of thumb: if the component looks and behaves fundamentally differently with the prop set one way vs another, make a separate example. |
schema.yml template:
name: My Component
version: "1.0"
component: MyComponent
file: client/src/components/MyComponent.tsx
description: Short description of the component purpose.
when_to_use: |
Guidance on when to pick this component.
variants:
default:
description: Default layout
best_for: General use
props:
title:
type: string
required: true
description: Main heading text
example: "Why Choose Us"
items:
type: array
required: true
description: List of items
items:
icon:
type: string
required: false
description: Tabler icon name (e.g., IconRocket)
label:
type: string
required: true
schema.ts template:
import { z } from "zod";
export const myItemSchema = z.object({
icon: z.string().optional(),
label: z.string(),
});
export const myComponentSectionSchema = z.object({
type: z.literal("my_component"),
version: z.string().optional(),
variant: z.enum(["default"]).optional(),
title: z.string(),
items: z.array(myItemSchema),
background: z.string().optional(),
});
export type MyItem = z.infer<typeof myItemSchema>;
export type MyComponentSection = z.infer<typeof myComponentSectionSchema>;
For multi-variant components, define one schema per variant and combine with z.union([...]).
2. Re-export in shared/schema.ts
Add a re-export block so the rest of the app can import the schema and types:
export {
myComponentSectionSchema,
type MyComponentSection,
} from "../4geeks-com/component-registry/my_component/v1.0/schema";
Also add the new section schema to the Section union type if one exists, or ensure it is included in the validation pipeline.
3. React Component โ client/src/components/<ComponentName>.tsx
(or a folder with index.ts + variant files for multi-variant components)
- Import types from the component registry schema (e.g.,
import type { MyComponentSection } from "..." or from @shared/schema).
- Accept a single
data prop typed to the section schema.
- No hardcoded content โ every piece of visible text, image, icon, URL, label, etc. MUST come from a YAML prop. The component renders only what the
data prop provides. Never hardcode strings, labels, or placeholder content inside the component itself.
- Use only semantic Tailwind tokens (
bg-primary, text-foreground, bg-muted, etc.) โ never hardcoded colors like bg-blue-500.
- Use
@tabler/icons-react for all icons โ never lucide-react.
- Use
UniversalImage for images (reference by image_id from the image registry).
- Use
UniversalVideo for video content.
- Buttons use
rounded-md (the default) โ do not override button border radius.
- Badges use
rounded-full so they render as proper pill-shaped badges.
- Container/wrapper padding โ when using any box element with a visual background (cards, panels, colored wrappers), always include inner padding so child elements never touch the wrapper borders. Most components use
p-card-padding (24px) or at minimum p-4/p-6.
- Follow the 4Geeks Design System rules below.
4. Register in SectionRenderer.tsx
In client/src/components/SectionRenderer.tsx:
-
Add an import at the top (in the appropriate eager/lazy section):
import { MyComponent } from "@/components/MyComponent";
-
Add a case in the renderSection switch:
case "my_component":
return <MyComponent data={section as Parameters<typeof MyComponent>[0]["data"]} />;
5. Register in DebugBubble Component Catalog
Add the new component to client/src/components/DebugBubble/utils/componentCatalog.ts so it appears in the DebugBubble's component section:
import { IconMyIcon } from "@tabler/icons-react";
{ type: "my_component", label: "My Component", icon: IconMyIcon, description: "Short description" },
6. ComponentPickerModal (Automatic)
The "Add Component" modal (client/src/components/editing/ComponentPickerModal.tsx) reads from the /api/component-registry API endpoint, which scans the 4geeks-com/component-registry/ folder automatically. As long as the component registry folder (step 1) exists with a valid schema.yml, the component will appear in the modal with no extra code changes.
7. YAML Content Example
Create at least one example page section in an existing page YAML (or the component's examples/ folder) so the component can be previewed:
- type: my_component
title: "Why Choose Us"
items:
- icon: IconRocket
label: "Fast-track your career"
4Geeks Design System Reference
All values below come from client/src/index.css and tailwind.config.ts. Components must use these tokens โ never raw hex/rgb values.
Brand Colors (Light Mode)
| Token | CSS Variable | Hex | Usage |
|---|
bg-background / text-foreground | --background / --foreground | #FFFFFF / #00041A | Page background, default text |
bg-primary / text-primary-foreground | --primary / --primary-foreground | #0084FF / #FFFFFF | Primary buttons, links, accents |
bg-accent / text-accent-foreground | --accent / --accent-foreground | #FFB718 / #00041A | Secondary highlight, badges |
bg-muted / text-muted-foreground | --muted / --muted-foreground | #FAFAFA / #737373 | Subtle backgrounds, secondary text |
bg-card / text-card-foreground | --card / --card-foreground | #FFFFFF / #00041A | Card surfaces |
bg-secondary / text-secondary-foreground | --secondary / --secondary-foreground | #F5F5F5 / #00041A | Outline buttons, secondary actions |
bg-destructive | --destructive | red | Errors, destructive actions |
Dark mode tokens are defined in .dark {} โ components automatically adapt via semantic classes.
Typography
| Element | Font | Size | Weight | Line Height | Letter Spacing |
|---|
H1 / .text-h1 | Lato (font-heading) | 50px (36px mobile) | 700 | 1.1 | -0.02em |
H2 / .text-h2 | Lato (font-heading) | 40px in CSS utility / 30px in Tailwind text-h2 token (28px mobile) | 700 | 1.2 | -0.01em |
Body / .text-body | Archivo (font-sans) | 16px | 400 | 1.6 | normal |
| Stats/Numbers | Inter Variable (.font-inter) | varies | varies | โ | โ |
Tailwind shortcuts: text-h1, text-h2, text-body, font-heading, font-sans.
Spacing
| Token | Value | Usage |
|---|
| Base unit | 8px (--spacing: 0.5rem) | Minimum spacing increment |
| Section spacing | 64px (spacing-section) | Vertical padding between sections |
| Card padding | 24px (spacing-card-padding) | Internal card padding |
Border Radius
| Token | Value |
|---|
rounded-card | 12px (0.75rem) โ brand standard for cards |
rounded-lg | 9px |
rounded-md | 6px |
rounded-sm | 3px |
Shadows
Use shadow-card for cards. Shadows are disabled in dark mode.
Icons
- Library:
@tabler/icons-react โ NEVER use lucide-react.
- Allowed icons: See
4geeks-com/theme.json icons array for the full approved list.
Allowed Backgrounds
4geeks-com/theme.json is the single source of truth for all allowed background colors. Never use a background color that is not in this file โ not even a color that "looks like" a brand color.
| ID | CSS Variable / Value | Hex equivalent (for mockup sandbox) |
|---|
background | --background | #FFFFFF |
muted | --muted | #FAFAFA |
card | --card | #FFFFFF |
secondary | --secondary | #F5F5F5 |
accent | --accent | #FFB718 |
primary | --primary | #0084FF |
sidebar | --sidebar-background | matches sidebar token |
light-blue-5 | hsl(210 100% 50% / 0.05) | ~#F0F7FF |
light-blue-5-gradient | gradient to transparent | โ |
light-blue-diagonal-gradient | diagonal gradient | โ |
Never use a text/foreground color as a background. --foreground (#00041A) is a text token โ it has no entry in the backgrounds list and must not be applied to background-color on any element.
Why this matters: Using off-list backgrounds โ even ones that look close โ breaks consistency across pages and makes dark mode adaptation unpredictable. The approved list was chosen to always look correct in both light and dark mode.
Allowed Text Colors
Text colors must also come from theme.json under text. Use solid values only โ never apply opacity or rgba(...) to text elements.
| ID | CSS Variable | Usage |
|---|
foreground | --foreground | Default body text, headings |
muted-foreground | --muted-foreground | Secondary/supporting text |
primary-foreground | --primary-foreground | Text on primary blue backgrounds |
secondary-foreground | --secondary-foreground | Text on secondary backgrounds |
primary | --primary | Colored links, CTAs, accent labels |
Why no transparency on text: Applying opacity: 0.35 or rgba(255,255,255,0.6) to text produces colors that are not part of the design system. They can clash with different backgrounds, break in dark mode, and create visual inconsistency across components. If you need a softer text color, use muted-foreground โ it exists exactly for that purpose.
Transparency on Backgrounds
Transparency is acceptable only on backgrounds, and only using the predefined opacity levels already codified in theme.json under courses:
| Pattern | Value | When to use |
|---|
primary/40 | hsl(var(--primary) / 0.4) | Stronger tinted bg |
primary/30 | hsl(var(--primary) / 0.3) | Medium tinted bg |
primary/20 | hsl(var(--primary) / 0.2) | Light tinted bg (icon wrappers when needed) |
primary/5 | hsl(210 100% 50% / 0.05) | Very subtle section wash (light-blue-5) |
Do not invent new rgba levels (e.g., rgba(0,132,255,0.15), rgba(0,0,0,0.06)). Use the closest approved value.
Color Philosophy โ Formal, Near-Monochromatic
4Geeks components lean almost entirely on blue and muted/neutral tones. The goal is a professional, restrained design that reads as credible and serious โ not colorful or consumer-facing.
Default approach:
- Backgrounds:
background, muted, card, or light-blue-5
- Text:
foreground and muted-foreground
- Accents:
primary (blue) โ used for interactive elements, links, icons, thin highlights
- Visual elevation/differentiation: use
primary/5 (a very subtle blue wash) rather than bold or dark backgrounds โ this applies broadly, not just to "featured" cards. Use it whenever you want a section, card, or element to stand out slightly or to add background variety without making it feel like a highlighted call-to-action
Accent yellow (--accent / #FFB718): Use very sparingly โ one deliberate use per component at most, such as a badge or a single highlight. Never use it as a repeating element across multiple items in a list or grid.
Why restraint matters: Too many colors in UI elements signals consumer/casual design. Mono-chromatic blue + muted tones communicates professionalism and focus, which aligns with 4Geeks as a serious tech education brand. When in doubt, reach for primary/5 before reaching for any other color.
Exception โ Program / Course Color Identities
When a component renders the 4 academy programs or courses as distinct side-by-side items that need visual differentiation, each program may be assigned one color identity from this fixed palette:
| Program | Color | Token | Hex (sandbox) |
|---|
| Full Stack Development with AI | Blue | --primary | #0084FF |
| AI Engineering | Gray | --muted-foreground | #737373 |
| Data Science & ML | Yellow | --accent | #FFB718 |
| Cybersecurity | Red | --destructive | hsl(0 75% 45%) โ #C0311B |
These color identities are used for: icon color, thin accent bars/lines, CTA link text color. They are not used as full card background colors (use card or light-blue-5 for card backgrounds). The assignment above is flexible โ the important thing is that each program consistently uses one distinct color across all components that reference it.
Why: When displaying all 4 programs simultaneously, pure mono-chrome makes them visually indistinguishable. Assigning one color per program lets users scan and remember which card is which โ especially useful when the same programs appear in multiple components across a page. The 4-color system (blue, gray, yellow, red) stays within the brand palette without becoming decorative or consumer-feeling.
Typography Sizing Hierarchy
Use font sizes to convey information hierarchy. There are three levels:
| Size | Tailwind | Usage |
|---|
text-base (16px) | text-base | Main body / description text โ any paragraph or tagline that carries real meaning. Do not downsize these to text-sm to save space. |
text-sm (14px) | text-sm | Supporting metadata โ duration badges, secondary labels, pill text, captions that are visually subordinate. |
text-xs (12px) | text-xs | Fine-print labels โ uppercase tracking-widest category tags, table column headers, "Avg. salary" type labels above a value. |
Common mistake to avoid: Using text-sm for a program tagline or card description because the card is small. Description text carries meaning and needs to be readable at normal size. If the card feels too crowded with text-base, the fix is to reduce padding or shorten the copy โ not to shrink the text.
Data values (numbers, salary ranges, durations displayed as standalone facts) should be sized up, not down โ they are the information the user came to read.
Why: Consistent text sizing creates a predictable reading rhythm across components. When description text is randomly text-sm in some components and text-base in others, the page feels inconsistent even if the colors and spacing are correct.
Icon Usage in Components
Icons are effective as visual differentiators for cards, list items, and bullet-like elements. Follow these rules:
- No background wrapper by default. Don't wrap icons in a colored circle or square container. The icon itself, rendered in
text-primary or text-muted-foreground, provides enough visual cue without adding visual noise.
- Max size:
w-8 h-8. Icons in cards and list items should not exceed 32px. Larger icons are reserved for hero/decorative use cases where the icon IS the main visual element.
- Exception โ sparse content: If a card or list item has very minimal content (e.g., just an icon and a single line of text, with no description or metadata), a subtle background on the icon (using
primary/20) can prevent the card from feeling visually empty. This is the only case where an icon background is acceptable.
Why: A background behind an icon in a rich card (one that already has a title, description, and CTA) adds unnecessary visual weight and makes the design feel busier. The icon's color alone is sufficient differentiation when the surrounding content provides enough structure.
Accent Separator Lines
When using a colored vertical bar or horizontal line as a visual differentiator (e.g., a left-side accent stripe on a card or list item):
- Keep it thin:
w-0.5 (2px) or w-1 (4px) for vertical bars, h-px or h-0.5 for horizontal dividers
- Never use
w-3 (12px) or wider โ this turns a subtle signal into a decorative block that competes with the content
Why: A thin line is enough to guide the eye and signal differentiation. A thick bar draws too much attention, creates visual imbalance, and undermines the formal, restrained aesthetic.
Button Variants
Use the shadcn <Button> component. The actual variant prop values are: default (renders as primary blue), secondary, outline, ghost, destructive. Note: in YAML content the CTA variant field uses "primary" which maps to variant="default" at the component level. Never implement custom hover/active states โ the built-in elevation system handles this.
Interaction System
- Use
hover-elevate and active-elevate-2 utility classes for non-Button/Badge elements.
- Never add
hover:bg-* or custom hover states to <Button> or <Badge>.
- For toggle states, use
toggle-elevate + toggle-elevated.
Naming Conventions
- Component type (YAML
type field): snake_case (e.g., graduates_stats, cta_banner)
- Component name (React):
PascalCase (e.g., GraduatesStats, CtaBanner)
- Registry folder: matches the
type value in snake_case (e.g., 4geeks-com/component-registry/graduates_stats/)
- Schema exports:
camelCase with Schema suffix (e.g., graduatesStatsSectionSchema)
- Type exports:
PascalCase with Section suffix (e.g., GraduatesStatsSection)
Multi-Variant Components
When a component supports multiple layout variants:
- Define a separate Zod schema per variant with a
variant literal discriminator.
- Combine into a unified schema with
z.union([...]).
- Create a main component file that switches on
variant and delegates to sub-components.
- Use a folder structure:
client/src/components/<component_name>/ with index.ts, main component, and variant files.
- Document each variant in
schema.yml under variants:.
Reference implementation: See graduates_stats component for the complete pattern:
client/src/components/graduates_stats/ โ folder with index, main component, and variant files
4geeks-com/component-registry/graduates_stats/v1.0/ โ schema.yml, schema.ts, field-editors.ts, examples/
Common Pitfalls
- Never hardcode content โ all visible text, labels, images, icons, URLs must come from YAML props. The component is a pure renderer of its
data prop.
- Never use hardcoded colors โ only semantic tokens (
bg-primary, text-muted-foreground, etc.).
- Never use
lucide-react โ always @tabler/icons-react.
- Never use raw
<img> or <video> tags โ use UniversalImage and UniversalVideo.
- Never use emoji โ use Tabler icons instead.
- Always include
data-testid attributes on interactive and meaningful display elements.
- Always support the
background prop โ most section components accept an optional background string for the outer wrapper.
- Always include
version: z.string().optional() in the Zod schema.
- Always re-export from
shared/schema.ts โ so the rest of the app can import types.
- Buttons use
rounded-md (the default border radius) โ never override it.
- Badges use
rounded-full โ so they render as proper pill shapes.
- Wrapper elements need padding โ any box with a visual background (card, panel, colored div) must have inner padding so children never touch the borders.
- Reuse existing field editors โ when a prop needs a custom inline editor, use one of the existing
EditorType values listed below. Only create a new editor type if the user explicitly asks for it.
Existing Field Editor Types
These are already implemented and available for use in field-editors.ts:
| EditorType | Purpose |
|---|
"icon-picker" | Pick a Tabler icon |
"color-picker" | Pick a theme color (variants: "color-picker:text", "color-picker:courses", "color-picker:accent") |
"image-picker" | Pick an image from the registry (variant: "image-picker:logo") |
"image-with-style-picker" | Pick an image with CSS positioning/style options |
"link-picker" | Pick/edit a URL link |
"video-picker" | Pick/edit a video URL |
"rich-text-editor" | Inline rich text editing |
"markdown" | Markdown text editing |
"font-size-picker" | Pick a font size from theme presets |
"boolean-toggle" | Toggle a boolean value |
"variant-picker" | Pick from component variants |
"cta-picker" | Edit a CTA button (text, url, variant) |
"text-input" | Simple text input |
"string-picker:opt1,opt2,..." | Pick from a custom list of string options |
Field Editor Key Path Convention
The keys in fieldEditors follow a convention based on where the prop lives in the data tree. The editor panel parses these paths at runtime โ using the wrong format will silently fail (the picker simply won't appear).
| Prop location | Key format | Example |
|---|
| Top-level section prop | Bare key | "layout", "show_salary" |
| Child of an array | "arrayName[].fieldName" | "programs[].color", "courses[].icon" |
| Variant-scoped array child | "variant:arrayName[].fieldName" | "solid:courses[].course_background" |
| Array-of-array grandchild | Not supported โ omit | โ |
Critical rule: array children must use [] bracket notation, not dot-star (programs.*.color) or plain dots (programs.color). The editor panel matches paths with the regex /^([\w.]+)\[\]\.(.+)$/ โ anything that doesn't match this pattern is ignored silently.
export const fieldEditors = {
layout: "string-picker:grid,stacked_list",
"programs[].color": "color-picker:courses",
"programs[].icon": "icon-picker",
};
export const fieldEditors = {
"programs.*.color": "color-picker:courses",
"programs.color": "color-picker:courses",
};