Applied Module 12 · AI-Accelerated Government Development

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.

💬CSP is not optional

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:

src/app/globals.css
@layer base, components, utilities;
LayerWhat goes hereSource
baseCSS resets, typography defaults, CSS custom propertiesTailwind @layer base
componentsMUI component styles, Emotion-generated CSSMUI 7 / Emotion
utilitiesTailwind 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.

Layer order matters

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:

src/app/globals.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;
}
No tailwind.config.js

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 v4
government 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 headers

What you get

Claude Code — building the component hierarchy
/home/user $ Build the application shell for our Next.js 15.5 + MUI 7 + Tailwind v4 government platform. [full prompt above]
I'll create the app shell with proper CSS layer ordering and CSP nonce injection. Created src/app/globals.css - @import "tailwindcss" with @layer base, components, utilities - @theme block with surface color scale and design tokens - Dark mode via prefers-color-scheme media query Created src/components/providers/ThemeProvider.tsx - MUI 7 dark theme synced with Tailwind tokens - Emotion CacheProvider with nonce from props - CssBaseline for consistent reset Updated src/app/layout.tsx - Reads nonce from x-csp-nonce header - Wraps children in ThemeProvider with nonce - Imports globals.css Created src/components/navigation/AppBar.tsx - Platform name, user role badge, sign-out - Hamburger menu on mobile (< 768px) Created src/components/navigation/Sidebar.tsx - MUI Drawer, 280px expanded / 64px collapsed - Persistent on desktop, temporary on mobile - Navigation links with active state highlighting All Emotion styles use the CSP nonce. No !important declarations.
/home/user $ How does the nonce get from middleware to the ThemeProvider?
The flow is: 1. **middleware.ts** generates a random nonce per request: ```typescript const nonce = crypto.randomUUID(); const cspHeader = `default-src 'self'; style-src 'self' 'nonce-${nonce}'; script-src 'self' 'nonce-${nonce}';`; response.headers.set('Content-Security-Policy', cspHeader); response.headers.set('x-csp-nonce', nonce); ``` 2. **layout.tsx** reads the nonce from headers: ```typescript import { headers } from 'next/headers'; export default async function RootLayout({ children }) { const headersList = await headers(); const nonce = headersList.get('x-csp-nonce') ?? ''; return ( <html lang="en"> <body> <ThemeProvider nonce={nonce}> {children} </ThemeProvider> </body> </html> ); } ``` 3. **ThemeProvider.tsx** creates an Emotion cache with the nonce: ```typescript const cache = createCache({ key: 'mui', nonce: nonce, }); ``` Every `<style>` tag Emotion injects will include `nonce="{value}"`, which the browser validates against the CSP header. Styles without the nonce are blocked.
/home/user $

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:

  1. Middleware generates a nonce — a random string, unique per request.
  2. CSP header includes the noncestyle-src 'self' 'nonce-{value}'.
  3. Emotion attaches the nonce — every <style> tag it creates carries nonce="{value}".
  4. Browser validates — styles with the matching nonce execute; styles without it are blocked.
src/components/providers/ThemeProvider.tsx
'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>
);
}
💡prepend: true matters

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:

src/components/providers/theme.ts
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.

Semantic HTML for accessibility

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

SymptomCauseFix
Tailwind p-4 has no effect on MUI ButtonMUI’s component styles have higher specificity than unsorted Tailwind utilitiesVerify @layer base, components, utilities order in globals.css
MUI Dialog backdrop is transparentTailwind’s base reset overrides MUI’s backdrop stylesWrap MUI backdrop styles in @layer components {}
Styles flash on page load (FOUC)Emotion styles load after Tailwind on first paintSet prepend: true on Emotion cache and verify SSR
Console error: “Refused to apply inline style”CSP blocking Emotion styles without noncePass nonce through layout to ThemeProvider’s Emotion cache
Dark mode flickers to light on navigationMUI theme not applied during server renderEnsure ThemeProvider wraps the entire app in layout.tsx, not in individual pages

KNOWLEDGE CHECK

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, utilities and 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 createTheme palette and Tailwind’s CSS custom properties must use the same color values.
  • prepend: true on 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.