GUI Styling & UX Guide
The official style guide for SveltyCMS components, ensuring a consistent and high-quality user experience.
Last updated: 11/4/2025
GUI Styling & UX Guide
This document is the single source of truth for all GUI, styling, and UX patterns within SveltyCMS. Adhering to this guide is mandatory for all contributions to ensure a cohesive, accessible, and performant user interface.
🏗️ Foundation: Skeleton + Tailwind
Our UI is built on a robust and flexible foundation:
- Skeleton.dev: We use Skeleton for core UI components (Buttons, Inputs, Modals, Toasts, Tabs, etc.) and its theming system.
- Tailwind CSS: We use Tailwind for all utility-first styling and layout.
- Custom Theme: We use a custom
sveltycmstheme (defined insveltycms.css) that provides all our semantic color palettes, includingprimary,secondary,tertiary,surface,error,warning,success, and pre-definedgradient-classes.
All development must use these provided semantic definitions. This allows for global theme changes and user-customization without refactoring component code.
🎨 Core Principles
Our GUI is built on a few unbreakable principles.
Use Semantic Theme Colors: This is the most important rule.
- ALWAYS use the semantic theme classes provided by Skeleton and our custom theme (e.g.,
bg-primary-500,text-error-500,border-surface-300). - NEVER use hardcoded colors (e.g.,
text-red-500,bg-blue-500,text-[#eee]). If you find yourself needing a color that isn’t in the theme, it’s a sign we need to update the theme, not bypass it.
100% Utility-Driven: All styling must be achieved using Tailwind utility classes or pre-defined Skeleton components/variants. No custom <style> blocks are permitted in Svelte components. Component-level styles (like gradients) are defined in the central theme file.
Svelte 5 Runes: All state management must use Svelte 5 Runes ($state, $derived, $effect). This ensures optimal performance and readability.
Dark Mode First: All components must include dark: variants for all styles. The UI must be perfectly usable in both light and dark modes.
Accessibility (A11y): Proper semantic HTML (<fieldset>, <legend>), ARIA attributes (aria-invalid, aria-describedby), and keyboard navigation support are required.
Performance: Avoid expensive calculations. Use $derived for computed values and memoization.
Consistency: Re-use the patterns defined in this guide. Do not invent new styling patterns for components that already have a defined style.
Layout & Spacing
Widget Containers
All form input widgets (Text, Relation, RichText, etc.) follow a standard layout.
- Wrapper:
relative mb-4 w-full - Error Spacing: Add
pb-6to the wrapper if (and only if) you are using the Pattern 1 (Absolute) Error Message. This padding provides the necessary space for the error text to appear without shifting layout.
<div class="relative mb-4 w-full pb-6">
{#if error}
<p class="absolute bottom-[-1rem] ...">
{error}
</p>
{/if}
</div>
⌨️ Form Inputs
We use two main styles for form inputs.
1. Standard Inputs (Text, Relation)
For simple inputs, we use a btn-group structure, often from Skeleton UI. This wraps the input and any related buttons (prefix, suffix, select) into a single, cohesive block.
- Container:
variant-filled-surface btn-group flex w-full items-center rounded - Input Element:
class="input w-full flex-1 rounded-none ..."
The rounded-none is critical to make it blend seamlessly with the group’s rounded container.
<div class="variant-filled-surface btn-group flex w-full items-center rounded">
<span class="min-w-0 flex-1 truncate p-2 text-left">
{displayText}
</span>
<div class="flex items-center">
<button type="button" class="btn variant-ghost-primary !rounded-none !px-3"> Select </button>
</div>
</div>
2. Large Inputs (RichText, MediaUpload)
For complex inputs that are their own self-contained “app” (like an editor or a dropzone), we use a single bordered container built with theme-aware surface colors.
- Container:
relative w-full rounded-lg border border-surface-300 bg-surface-100 shadow-sm dark:border-surface-700 dark:bg-surface-800 - Internal Structure: Use
border-b dark:border-surface-700to separate internal parts like toolbars and title inputs.
<div class="relative w-full rounded-lg border border-surface-300 dark:border-surface-700 ... pb-6 ...">
<input type="text" class="w-full border-b border-surface-300 dark:border-surface-700 ..." />
<div class="toolbar flex flex-wrap ... border-b border-surface-300 dark:border-surface-700 ..."></div>
<div bind:this={element} class="editor-content prose ..."></div>
</div>
Validation & Error Handling
This is one of the most important aspects of our GUI. We use three distinct patterns for error messaging. Choose the correct one for your component’s context.
Pattern 1: Absolute Error Text (Default)
This is the preferred method for most inputs. The error text appears below the input without affecting document flow.
- When to Use: Text, Relation, RichText, and any input that is a single, contained block.
- Container Class: The input wrapper must have
pb-6. - Error Classes:
absolute bottom-[-1rem] left-0 w-full text-center text-xs text-error-500 - Input Error State:
class:!border-error-500={!!error}and/orclass:!ring-1={!!error} class:!ring-error-500={!!error}. The!prefix is required to override Skeleton’s specificity.
<div class="relative ... pb-6">
<input class="input" class:!border-error-500={!!error} ... />
{#if error}
<p class="absolute bottom-[-1rem] left-0 w-full text-center text-xs text-error-500" role="alert">
{error}
</p>
{/if}
</div>
Pattern 2: Static Error Text
This method is for components where an absolute position is awkward, such as radio groups or empty dropzones. The error text pushes content down.
- When to Use: Radio groups, Checkbox groups, empty mediaUpload inputs.
- Container Class: No
pb-6needed. - Error Classes:
mt-2 text-center text-xs text-error-500(ortext-left). - Input Error State: Apply the error state to the most logical element (e.g., the
<legend>or the container border).
<fieldset>
<legend class="..." class:!text-error-500={!!error}> Select an Option </legend>
</fieldset>
{#if error}
<p class="mt-2 text-center text-xs text-error-500" role="alert">
{error}
</p>
{/if}
Pattern 3: Error Banner
This method is for complex, multi-part components where a single line of text is insufficient or would look out of place.
- When to Use: megaMenu builder, GuiFields builder, complex list inputs.
- Error Classes:
flex items-center gap-2 rounded-lg border border-error-200 bg-error-50 p-3 text-error-700 dark:border-error-800 dark:bg-error-900/20 dark:text-error-300
{#if error}
<div
class="flex items-center gap-2 rounded-lg border border-error-200 bg-error-50 p-3 text-error-700 dark:border-error-800 dark:bg-error-900/20 dark:text-error-300"
role="alert"
>
<iconify-icon icon="mdi:alert-circle" width="16"></iconify-icon>
{error}
</div>
{/if}
🔘 Buttons & Actions
- Primary Action:
class="... btn variant-filled-primary"(e.g., “Save”). - Secondary Action:
class="... btn variant-filled-secondary"(e.g., “YouTube”). - Tertiary/Brand Action:
class="... btn variant-filled-tertiary"(e.g., “Add New”). - Ghost Action:
class="... btn variant-ghost-surface"(e.g., toolbar buttons). - Destructive Action:
class="... btn variant-ghost-error"or a hover effectclass="... hover:text-error-500 dark:hover:text-error-400". - Gradient Buttons: For special CTAs, use the pre-defined gradient classes (e.g.,
gradient-primary,gradient-tertiary). These are defined in the main theme file and include hover/focus states.
🌫️ Overlays (Modals & Dropdowns)
Overlays use a consistent “frosted glass” effect.
Modals
- Overlay: A fixed backdrop with
bg-black/30 backdrop-blur-sm. - Panel: A fixed
z-50 ... rounded-lg bg-white dark:bg-surface-800 p-6 shadow-xl.
{#if show}
<div class="fixed inset-0 z-40 bg-black/30 backdrop-blur-sm" onclick={close}></div>
<div class="fixed left-1/2 top-1/2 z-50 ... rounded-lg bg-white p-6 shadow-xl dark:bg-surface-800"></div>
{/if}
Dropdowns & Pop-ups
Dropdowns use a semi-transparent, blurred background to hint at the content underneath.
- Container:
relative. - Panel:
absolute z-20 mt-1 ... rounded-md border border-surface-400/30 bg-surface-50/95 shadow-lg backdrop-blur-sm focus:outline-none dark:border-surface-300/20 dark:bg-surface-800/90
The key classes are bg-surface-50/95 dark:bg-surface-800/90 and backdrop-blur-sm.
🌙 Dark Mode
Dark mode is not optional. Every class that sets a color, background, or border must have a corresponding dark: variant, and these must use the theme’s semantic colors.
- Borders:
border-surface-300 dark:border-surface-700 - Backgrounds:
bg-surface-100 dark:bg-surface-800 - Text:
text-surface-700 dark:text-surface-200 - Prose:
prose dark:prose-invert
✅ Style Checklist
Before submitting a PR, check for:
[ ] Semantic Colors: Component uses theme colors (e.g., bg-primary-500, text-error-500) and not hardcoded Tailwind colors (e.g., bg-blue-500, text-red-500).
[ ] No <style> blocks: All styles are 100% Tailwind utility classes.
[ ] Dark Mode: All components are tested and look correct in dark mode.
[ ] Error Handling: The correct error pattern (Absolute, Static, or Banner) is used.
[ ] Responsiveness: The component is mobile-first and responsive.
[ ] Accessibility: All buttons and inputs have labels, and ARIA roles are used where needed.
[ ] Svelte 5 Runes: No Svelte 4 syntax is used (export let, onMount, etc., unless truly necessary).