Secure App Shell
What you'll learn
~35 min- Configure MUI 7 and Tailwind v4 CSS layers to prevent style conflicts
- Implement CSP nonce injection for Emotion CSS-in-JS styles
- Build a responsive app shell with dark mode that satisfies accessibility requirements
Two styling systems, one shell
The DS platform uses MUI 7 for structured components (DataGrid, Autocomplete, Dialog) and Tailwind v4 for utility styling (spacing, layout, responsive breakpoints). These two systems generate CSS that can conflict if you do not control the cascade. CSS layers solve this.
This lesson builds the app shell — AppBar, Sidebar, main content area — with both systems cooperating under a Content Security Policy that blocks inline styles unless they carry a server-generated nonce.
State security audits flag inline styles and scripts without nonces. A style-src 'unsafe-inline' directive in your CSP header is a finding. Emotion (MUI’s CSS-in-JS engine) injects <style> tags at runtime. Without nonce injection, your CSP either blocks MUI’s styles (breaking the UI) or allows all inline styles (failing the audit). This lesson handles it correctly.
CSS layer architecture
CSS @layer declarations control specificity without !important hacks. Styles in a later layer override earlier layers regardless of selector specificity. The DS platform defines three layers in this order:
@layer base, components, utilities;| Layer | What goes here | Source |
|---|---|---|
base | CSS resets, typography defaults, CSS custom properties | Tailwind @layer base |
components | MUI component styles, Emotion-generated CSS | MUI 7 / Emotion |
utilities | Tailwind utility classes (flex, p-4, text-sm) | Tailwind @layer utilities |
Because utilities is declared last, a Tailwind class like p-4 will always override MUI’s default padding — which is exactly what you want when you are fine-tuning layout.
If you put utilities before components, MUI’s styles will override your Tailwind overrides. The order base, components, utilities means Tailwind utilities always win. This is the correct configuration for platforms that use MUI as the base and Tailwind for customization.
Tailwind v4 configuration
Tailwind v4 uses a CSS-first configuration model. There is no tailwind.config.js file. Configuration lives in your CSS:
@import "tailwindcss";
@layer base, components, utilities;
@theme { --color-surface-50: #f8fafc; --color-surface-900: #0f172a; --color-primary: #3b82f6; --color-danger: #ef4444; --color-success: #22c55e;
--font-sans: "Inter", system-ui, sans-serif; --radius-md: 0.5rem;}Tailwind v4 moved configuration into CSS via @theme and @import. If the AI generates a tailwind.config.js or tailwind.config.ts file, it is using v3 patterns. Reject and re-prompt specifying Tailwind v4 CSS-first config.
The prompt
Build the application shell for our Next.js 15.5 + MUI 7 + Tailwind v4government platform. Requirements:
1. GLOBALS CSS (src/app/globals.css) - @import "tailwindcss" - @layer base, components, utilities (in this order) - @theme block with color tokens: surface-50 through surface-900, primary, danger, success - Dark mode custom properties using @media (prefers-color-scheme: dark) - No tailwind.config.js — Tailwind v4 CSS-first config only
2. THEME PROVIDER (src/components/providers/ThemeProvider.tsx) - MUI 7 createTheme with dark palette matching Tailwind tokens - CssBaseline for reset - Emotion CacheProvider with CSP nonce from server - Accept nonce as prop, create Emotion cache with nonce option - "use client" directive
3. ROOT LAYOUT (src/app/layout.tsx) - Import globals.css - Read CSP nonce from response headers (generated in middleware) - Pass nonce to ThemeProvider - Include AppBar and Sidebar as server components wrapping client shell
4. APP BAR (src/components/navigation/AppBar.tsx) - MUI AppBar with platform name, user avatar from session, sign-out button - Role badge showing current user role (from session) - Dark background: surface-900 - Responsive: hamburger menu on mobile
5. SIDEBAR (src/components/navigation/Sidebar.tsx) - Collapsible sidebar with navigation links grouped by module - Active link highlighting - Width: 280px expanded, 64px collapsed (icon-only) - Persistent on desktop, drawer on mobile - Uses MUI Drawer component
6. MAIN LAYOUT - Sidebar + content area with proper flex layout - Content area: max-width 1280px, padding responsive - Scroll behavior: sidebar fixed, content scrolls
CSS LAYER RULES:- MUI/Emotion styles must render into the "components" layer- Tailwind utilities must win over MUI defaults- No !important anywhere
CSP RULES:- Emotion cache must use the nonce for style injection- The nonce is generated per-request in middleware and passed via headersWhat you get
CSP nonce injection deep dive
Content Security Policy prevents cross-site scripting by blocking inline styles and scripts unless they are explicitly allowed. The DS platform uses nonce-based allowlisting:
- Middleware generates a nonce — a random string, unique per request.
- CSP header includes the nonce —
style-src 'self' 'nonce-{value}'. - Emotion attaches the nonce — every
<style>tag it creates carriesnonce="{value}". - Browser validates — styles with the matching nonce execute; styles without it are blocked.
'use client';
import { CacheProvider } from '@emotion/react';import createCache from '@emotion/cache';import { ThemeProvider as MuiThemeProvider, CssBaseline } from '@mui/material';import { darkTheme } from './theme';
interface Props { nonce: string; children: React.ReactNode;}
export default function ThemeProvider({ nonce, children }: Props) { const cache = createCache({ key: 'mui', nonce, // Every <style> tag gets this nonce prepend: true, // Emotion styles before Tailwind in DOM order });
return ( <CacheProvider value={cache}> <MuiThemeProvider theme={darkTheme}> <CssBaseline /> {children} </MuiThemeProvider> </CacheProvider> );}Setting prepend: true on the Emotion cache inserts MUI’s styles before Tailwind’s in the DOM. Combined with the @layer declaration order (components before utilities), this guarantees Tailwind utilities override MUI defaults without !important.
Dark mode: MUI + Tailwind together
Dark mode on the DS platform is not a toggle — it is the default. Government operations staff often work in low-light environments (operations centers, server rooms during maintenance windows). The theme is dark-first.
MUI and Tailwind need to agree on the dark palette:
import { createTheme } from '@mui/material/styles';
export const darkTheme = createTheme({ palette: { mode: 'dark', background: { default: '#0f172a', // matches --color-surface-900 paper: '#1e293b', // matches --color-surface-800 }, primary: { main: '#3b82f6', // matches --color-primary }, error: { main: '#ef4444', // matches --color-danger }, }, typography: { fontFamily: 'var(--font-sans)', },});Tailwind dark mode classes (dark:bg-surface-900, dark:text-white) and MUI’s palette both point to the same color values. No conflicts.
The shell layout
// Simplified layout structure
<body className="bg-surface-900 text-white"> <AppBar /> {/* Fixed top, z-50 */} <div className="flex pt-16"> {/* Below AppBar */} <Sidebar /> {/* Fixed left, w-[280px] */} <main className="flex-1 ml-[280px] p-6 max-w-7xl"> {children} {/* Page content */} </main> </div></body>The sidebar is fixed-position. The main content area scrolls independently. On screens narrower than 768px, the sidebar collapses to a hamburger-triggered drawer and the ml-[280px] is removed.
The shell uses <header> for the AppBar, <nav> for the Sidebar, <main> for the content area, and <aside> where appropriate. Screen readers use these landmarks to navigate. MUI’s AppBar renders a <header> by default; the Drawer renders a <nav>. Verify these are not overridden with component="div" in the generated code.
Common CSS conflicts and fixes
| Symptom | Cause | Fix |
|---|---|---|
Tailwind p-4 has no effect on MUI Button | MUI’s component styles have higher specificity than unsorted Tailwind utilities | Verify @layer base, components, utilities order in globals.css |
| MUI Dialog backdrop is transparent | Tailwind’s base reset overrides MUI’s backdrop styles | Wrap MUI backdrop styles in @layer components {} |
| Styles flash on page load (FOUC) | Emotion styles load after Tailwind on first paint | Set prepend: true on Emotion cache and verify SSR |
| Console error: “Refused to apply inline style” | CSP blocking Emotion styles without nonce | Pass nonce through layout to ThemeProvider’s Emotion cache |
| Dark mode flickers to light on navigation | MUI theme not applied during server render | Ensure ThemeProvider wraps the entire app in layout.tsx, not in individual pages |
Your Next.js application uses MUI 7 and Tailwind v4. You declare CSS layers in this order: @layer base, components, utilities. A MUI Button has default padding of 16px. You add the Tailwind class p-2 (8px) to the same Button. What padding does the Button render?
Key takeaways
- CSS layers eliminate MUI vs. Tailwind conflicts. Declare
@layer base, components, utilitiesand Tailwind utilities will always override MUI defaults without!important. - CSP nonce injection is required for Emotion CSS-in-JS in government deployments. Generate a per-request nonce in middleware, pass it through the layout to the Emotion cache.
- Tailwind v4 uses CSS-first configuration. No
tailwind.config.js. Tokens go in@theme {}blocks inside your CSS file. If the AI generates a config file, it is using v3 patterns. - Dark mode is the default, not an option. MUI’s
createThemepalette and Tailwind’s CSS custom properties must use the same color values. prepend: trueon the Emotion cache ensures MUI styles appear before Tailwind in the DOM, working with the layer declaration order to give Tailwind utilities the final word.
What’s next
The shell is styled and secured. The next lesson adds role-based access control — the 5-role RBAC system that determines what each user can see and do inside this shell. You will build the withPermission() higher-order function that wraps every API route.