SGUI / Development
Theming
How global decisions become semantic roles, component styles and scoped overrides.
The framework
A theme is a set of decisions and relationships. A color palette supplies the available colors; semantic roles explain their purpose; component tokens connect that purpose to a particular control. The application chooses where those decisions apply.
Global theme decisions
Identity + appearance + contrast
Typography + shape + spacing + density
↓
Named color palettes → semantic mappings → component tokens → component styles
↓
Application scope → nested scopes and targeted overridesThe dependency direction stays consistent. Components use named roles through their own tokens; they do not choose raw palette colors themselves.
1. Global theming
Global theming establishes the shared contract for an application: its visual identity, typography, shape, spacing, default density, and policies for appearance and contrast. It provides a coherent starting point for every component in that application scope.
The demo theme “None — global defaults” shows the library’s built-in styles without a demo identity override. It retains SGUI typography, spacing, shape, states and accessible controls; it does not remove the stylesheet. Defaults already cover primary actions, neutral actions, surfaces, text, focus, error, warning, success and information. The warning role is amber, errors are red, success is green and informational messaging uses blue. These semantic roles are separate from primary/secondary/tertiary action priority.
Keep these decisions independent. Identity describes the visual language. Appearance chooses light or dark surfaces. Contrast chooses standard or increased contrast. Density changes spacing and control geometry. Changing one should not require rebuilding the others.
Interaction sizing is independent of density and viewport layout. interactionMode="auto" follows available pointer capabilities; pointer and touch are explicit host overrides. Touch sizing enlarges control targets and navigation icons while keeping the chosen typography and density. A tablet with a keyboard or a touchscreen laptop can support multiple inputs at once.
<Provider density="compact" interactionMode="auto"
style={{
"--sgui-compact-control-height": "2rem",
"--sgui-comfortable-control-height": "2.75rem",
"--sgui-touch-target-size": "2.75rem",
"--sgui-touch-navigation-icon-size": "1.25rem"
}}>
{/* Application workspace */}
</Provider>Global means the application-level default. It does not mean styling every element in the document. SGUI uses explicit scopes, so an application can host a differently themed region without changing the surrounding interface.
2. Semantic theming
Semantic roles give the interface a stable vocabulary of intent: surface, text, muted text, action, focus, selection, and validation. A role describes what a color does. It does not describe its hue or the name of a component.
surface / surfaceRaised / surfaceOverlay → layers of the interface
text / textMuted → information hierarchy
action / actionHover / actionPressed / onAction → an action and its states
surfaceSelected / textSelected / borderSelected → a selection treatment
validationError / validationSuccess / focus → meaning and feedbackRoles work in pairs and groups. An action background needs readable onAction text; a selected surface needs readable selected text; focus must remain visible against its surrounding surface. Those relationships remain stable even when the underlying colors change.
3. Color palettes
A palette is the named inventory of raw colors available to a theme: navy, gold, white, cool-gray shades, and supporting colors. The names belong to the palette, not to a particular button, field, or page.
A palette alone does not say which color is a link, a background, or a focus indicator. Keeping values separate from their use lets the same palette support multiple appearances and contrast levels. Typography and shape have their own identity settings; they are not color-palette entries.
4. From palettes to semantics
The semantic map assigns palette colors to roles for a particular identity, appearance, and contrast combination. This is a deliberate design decision, not an automatic lightening or darkening formula. Each combination needs a complete mapping and verified foreground/background pairs.
One palette, two mappings:
Light: action → navy; onAction → white
Dark: action → light gold; onAction → navy
One component dependency:
buttonPrimaryBackground → action → palette value
buttonPrimaryText → onAction → palette valueComponent tokens add the bridge to implementation. A button background references an action role, while a field background references a surface role. Component styles consume those aliases. Change the action role and all components that depend on it can change together; change only the button token and the effect stays specific to buttons.
Interaction states need their own considered mappings. Increased contrast strengthens the relevant pairs. Operating-system forced colors take priority over the selected palette and use system colors so the interface remains usable.
5. Application
The application owns its identity palettes, semantic mappings, and the policy that selects the active appearance and contrast. SGUI supplies the component contracts, default tokens, styles, and scope mechanism. The resolved semantic values are applied as CSS variables, and the component aliases resolve against that scope.
Load the SGUI stylesheet once. Use Provider at the application boundary for the owned visual scope and interaction-locale integration. Use ThemeScope for a nested visual boundary, such as a compact region or a differently themed preview. The host owns document backgrounds, font loading, and application-level preference handling.
A scope also defines where customization stops. Theme variables follow nested scopes and SGUI overlay portals; layout styles such as padding stay on their source element. This allows a dialog or menu to match the region that opened it while the surrounding application retains its own theme.
This website demonstrates that separation: its navigation and documentation use one fixed application identity, while component previews use independently selected demo settings. Those examples exercise the same framework.
6. Overrides
Start with a component’s supported props when they express the intended variant or state. For a theme change, choose the smallest layer that matches the desired reach.
Palette change: update a raw color when every semantic role that references that color should follow the change.
Semantic override: change a role within a scope when every component using that intent should change, such as the action treatment throughout one application region.
Component-token override: change a component alias when one component family needs a different treatment while the shared semantics stay intact.
Local scope: place the override on Provider or ThemeScope around the region that needs it. Nested scopes retain inherited theme variables and can replace individual values. Sibling regions outside that scope keep their own values.
Override related states together: background, hover, pressed, foreground, and any affected focus treatment. Verify the resulting contrast and forced-color behavior. Native className/style remain available for context-specific layout; reusable visual decisions belong in the theme or component tokens.
Inspect the token pipeline
Choose a component token to follow its dependency through a semantic role to a named palette value. These references come from the library’s compiled public tokens; the active scope supplies the mapping.
component.buttonPrimaryBackground → semantic.action → palette.material.primary40 → #6750a4--sgui-button-primary-background: var(--sgui-action);Color tokens resolve through the active semantic map to a named palette value. Geometry and typography reference shared or density tokens. Component styles consume these aliases; nested scopes and portals retain the local theme.
{
"surface": "palette.material.neutral99",
"surfaceSubtle": "palette.material.neutralVariant95",
"surfaceRaised": "palette.material.neutralVariant97",
"surfaceOverlay": "palette.material.neutralVariant97",
"surfaceHover": "palette.material.neutralVariant92",
"surfacePressed": "palette.material.neutralVariant90",
"text": "palette.material.neutral10",
"textMuted": "palette.material.neutralVariant35",
"border": "palette.material.neutralVariant50",
"borderSubtle": "palette.material.neutralVariant50",
"divider": "palette.material.neutralVariant50",
"action": "palette.material.primary40",
"actionHover": "palette.material.primary30",
"actionPressed": "palette.material.primary35",
"onAction": "palette.shared.white",
"focus": "palette.material.primary40",
"textSubtle": "palette.shared.gray600",
"danger": "palette.shared.red700",
"backdrop": "palette.shared.slate900Alpha50",
"textInverse": "semantic.surface",
"textDisabled": "semantic.textMuted",
"separator": "semantic.divider",
"actionNeutral": "semantic.text",
"actionNeutralHover": "semantic.textMuted",
"actionNeutralPressed": "semantic.text",
"onActionNeutral": "semantic.surface",
"actionDestructive": "semantic.danger",
"onActionDestructive": "semantic.onAction",
"surfaceSelected": "semantic.surfaceSubtle",
"textSelected": "semantic.text",
"borderSelected": "semantic.action",
"surfaceDisabled": "semantic.surfaceSubtle",
"borderDisabled": "semantic.divider",
"validationError": "semantic.danger",
"validationInfo": "semantic.action",
"validationSurface": "semantic.surfaceSubtle",
"focusOnRaised": "semantic.focus",
"focusOnOverlay": "semantic.focus",
"actionDestructiveHover": "palette.shared.red800",
"actionDestructivePressed": "palette.shared.red800",
"validationSuccess": "palette.shared.green900",
"validationWarning": "palette.shared.amber800"
}{
"primary20": "#381e72",
"primary30": "#57418c",
"primary35": "#4f378b",
"primary40": "#6750a4",
"primary70": "#b69df8",
"primary80": "#d0bcff",
"primary90": "#eaddff",
"purple900": "#3b2068",
"neutral10": "#1c1b1f",
"neutralVariant15": "#2b2930",
"neutralVariant20": "#302d38",
"neutralVariant25": "#3a3544",
"neutralVariant30": "#443d50",
"neutralVariant35": "#49454f",
"neutralVariant50": "#79747e",
"neutralVariant60": "#938f99",
"neutralVariant80": "#cac4d0",
"neutral90": "#e6e1e5",
"neutralVariant90": "#e6dfea",
"neutralVariant92": "#ece5f2",
"neutralVariant95": "#f3edf7",
"neutralVariant97": "#f7f2fa",
"neutral99": "#fffbfe"
}Component token override
A local primary-button override uses the identity’s neutral action colors. The sibling button and field keep their existing tokens. Nested density and the menu portal preserve the same scoped override.
'use client';
import { useState } from 'react';
import { ThemeScope } from '@structured-growth/sg-ui/theme';
import { Button, Dialog, Menu, TextField } from '@structured-growth/sg-ui/experimental';
import { Stack } from '@structured-growth/sg-ui/primitives';
export default function ComponentTokenOverrideExample() {
const [open, setOpen] = useState(false);
return (
<>
<Stack gap={3}>
<Button>Identity button</Button>
<ThemeScope density="compact" style={{}}>
<Stack gap={3}>
<Button>Scoped button</Button>
<TextField label="Unchanged field" defaultValue="Same identity semantics" />
<Menu label="Scoped actions" trigger={<Button variant="outlined">Open scoped menu</Button>} items={[{id:'save',label:'Save'}]} />
<Button variant="outlined" onPress={() => setOpen(true)}>Open scoped dialog</Button>
<Dialog open={open} title="Scoped dialog" onDismiss={() => setOpen(false)}><Button onPress={() => setOpen(false)}>Portal action</Button></Dialog>
</Stack>
</ThemeScope>
</Stack>
</>
);
}
Production foundations
These examples consume the library stylesheet and production token references. Each panel owns its controls. Appearance follows the preview preferences; spacing, focus and motion use the shipped contracts.
Production spacing scale
Spacing follows the shared rem-based production scale. Enlarge browser text to inspect how bars, padding and gaps grow together.
var(--sgui-space1)
Padding and gap use the same production spacing step.
var(--sgui-space2)
Padding and gap use the same production spacing step.
var(--sgui-space3)
Padding and gap use the same production spacing step.
var(--sgui-space4)
Padding and gap use the same production spacing step.
import {Box, Stack, Typography} from '@structured-growth/sg-ui/primitives';
import {Surface} from '@structured-growth/sg-ui/experimental';
import {tokens} from '@structured-growth/sg-ui/tokens';
export function SpacingScale() {
return <Stack gap={4}>
{([1, 2, 3, 4] as const).map(step => <Surface key={step} variant="outlined" padding={step}>
<Stack gap={step}>
<Typography variant="code">{tokens[`space${step}`]}</Typography>
<Box aria-hidden="true" style={{inlineSize: tokens[`space${step}`], blockSize: tokens.space4, background: tokens.action}} />
<Typography>Padding and gap use the same production spacing step.</Typography>
</Stack>
</Surface>)}
</Stack>;
}Keyboard focus traversal
Use Tab and Shift+Tab through the three enabled controls. The disabled action is skipped. Focus styling comes from the production controls; test forced colors through your operating system as well.
import {Button} from '@structured-growth/sg-ui/components/Button';
import {Stack} from '@structured-growth/sg-ui/primitives';
import {TextField} from '@structured-growth/sg-ui/experimental';
export function FocusTraversal() {
return <Stack direction="row" wrap gap={3} align="end">
<Button>First focus target</Button>
<Button disabled>Unavailable focus target</Button>
<TextField label="Editable focus target" defaultValue="Course draft" />
<Button variant="outlined" tone="neutral">Last focus target</Button>
</Stack>;
}Motion and system preferences
Change your system reduced-motion preference while this panel is open. Production CSS removes transitions and indeterminate animation, while progress semantics and pending interaction remain. This sample shows host-supplied pending state; it performs no operation.
import {Button} from '@structured-growth/sg-ui/components/Button';
import {Stack} from '@structured-growth/sg-ui/primitives';
import {Progress} from '@structured-growth/sg-ui/experimental';
export function MotionPreferences() {
return <Stack gap={3}>
<Stack direction="row" wrap gap={3}>
<Button>Hover or focus action</Button>
<Button loading>Pending action</Button>
</Stack>
<Progress label="Indeterminate linear work" />
<Progress label="Indeterminate circular work" variant="circular" />
<Progress label="Determinate work" value={40} valueText="40 of 100" />
</Stack>;
}