PowerShell HTML Report: Build Styled Reports with CSS

PowerShell HTML Report: Build Styled Reports with CSS

PowerShell Tips Editor 3 min read
PowerShell HTML Report: Build Styled Reports with CSS

A plain-text PowerShell output pasted into an email tells the story but loses the audience. A styled HTML report with color-coded status rows, bold headers, and a clean table layout communicates the same data in a format executives and operations staff can scan in seconds. PowerShell generate HTML report using ConvertTo-Html with custom CSS gives you polished, professional output without any extra modules. This post covers basic conversion, CSS styling, conditional row colors, multi-table reports, and emailing the result.

ConvertTo-Html Basic Usage

ConvertTo-Html converts PowerShell objects to an HTML table string. Pass it the properties you want as columns and it handles the markup:

$data = Get-Service | Where-Object StartType -eq Automatic |
    Select-Object Name, DisplayName, Status

$html = $data | ConvertTo-Html -Title "Service Status Report" -PreContent "<h2>Automatic Services</h2>"

$html | Out-File "C:\Reports\services.html" -Encoding UTF8
Write-Host "Report saved"

The output is a complete HTML document with <html>, <head>, and <body> tags. The -PreContent and -PostContent parameters inject HTML before and after the table.

Add CSS Styling with -Head

Inject a CSS style block via the -Head parameter to transform the default browser-style table into a polished report:

$css = @"
<style>
  body  { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; margin: 20px; }
  h2    { color: #0078d4; border-bottom: 2px solid #0078d4; padding-bottom: 4px; }
  table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
  th    { background-color: #0078d4; color: white; padding: 8px 12px; text-align: left; }
  td    { padding: 6px 12px; border-bottom: 1px solid #e0e0e0; }
  tr:nth-child(even) td { background-color: #f5f5f5; }
</style>
"@

$html = $data | ConvertTo-Html -Head $css -Title "Service Report" -PreContent "<h2>Automatic Services</h2>"
$html | Out-File "C:\Reports\services-styled.html" -Encoding UTF8

Conditional Row Color with PostContent

PowerShell’s ConvertTo-Html does not natively support per-row CSS classes. The practical workaround is to post-process the HTML string and inject class attributes using string replacement:

$css = @"
<style>
  body { font-family: Segoe UI, sans-serif; font-size: 13px; margin: 20px; }
  th   { background:#0078d4; color:white; padding:8px 12px; }
  td   { padding:6px 12px; border-bottom:1px solid #ddd; }
  .stopped { background-color: #ffe0e0; }
  .running { background-color: #e0ffe0; }
</style>
"@

$html = Get-Service | Where-Object StartType -eq Automatic |
    Select-Object Name, DisplayName, Status |
    ConvertTo-Html -Head $css -PreContent "<h2>Service Status</h2>"

# Inject CSS classes by replacing table cell content patterns
$html = $html -replace '<td>Stopped</td>', '<td class="stopped">Stopped</td>'
$html = $html -replace '<td>Running</td>', '<td class="running">Running</td>'

$html | Out-File "C:\Reports\services-color.html" -Encoding UTF8

Multiple Tables in One Report

Combine multiple data sets into a single HTML report using -PreContent to separate sections and string concatenation:

$svcHtml  = Get-Service | Where-Object Status -eq Stopped |
    Select-Object Name, Status |
    ConvertTo-Html -Fragment -PreContent "<h2>Stopped Services</h2>"

$diskHtml = Get-PSDrive -PSProvider FileSystem |
    Select-Object Name, @{N='FreeGB';E={[Math]::Round($_.Free/1GB,1)}},
        @{N='UsedGB';E={[Math]::Round($_.Used/1GB,1)}} |
    ConvertTo-Html -Fragment -PreContent "<h2>Disk Space</h2>"

$report = ConvertTo-Html -Head $css -Title "Server Report" `
    -PreContent "<h1>$env:COMPUTERNAME — $(Get-Date -Format 'yyyy-MM-dd')</h1>" `
    -Body ($svcHtml + $diskHtml)

$report | Out-File "C:\Reports\combined-report.html" -Encoding UTF8

Embed Charts with Pre-Built HTML

For simple bar charts without JavaScript libraries, embed a CSS-only chart using absolute-width div elements scaled to percentages:

$diskBars = Get-PSDrive -PSProvider FileSystem | ForEach-Object {
    $usedPct = if ($_.Used + $_.Free -gt 0) {
        [int](($_.Used / ($_.Used + $_.Free)) * 100)
    } else { 0 }
    "<p><strong>$($_.Name):</strong> <span style='display:inline-block;width:${usedPct}%;background:#0078d4;color:white;padding:2px 4px'>${usedPct}%</span></p>"
}

$chartSection = "<h2>Disk Usage</h2>" + ($diskBars -join '')

Save and Email the Report

Save the HTML file and send it as an HTML email body or attachment:

$reportPath = "C:\Reports\server-report-$(Get-Date -Format 'yyyyMMdd').html"
$report | Out-File $reportPath -Encoding UTF8

# Send as HTML email body
$emailBody = $report | Out-String

Send-MailMessage -From "[email protected]" -To "[email protected]" `
    -Subject "Server Report $(Get-Date -Format 'yyyy-MM-dd')" `
    -Body $emailBody -BodyAsHtml `
    -SmtpServer "smtp-relay.corp.com"

Write-Host "Report emailed"

Common Errors and Fixes

  • ConvertTo-Html outputs string — must set encoding on Out-File. The default Out-File encoding on Windows PowerShell 5.1 is UTF-16 LE, which browsers may not render correctly for reports containing special characters. Always use -Encoding UTF8 with Out-File for HTML reports.
  • CSS in -Head must be inside <style> tags. Passing raw CSS without the <style>...</style> wrapper to -Head results in the CSS text appearing as visible body content in the browser. Always wrap your CSS in a proper <style> element.

Related Cmdlets / See Also

Wrapping Up

ConvertTo-Html with a CSS -Head block produces professional HTML reports from any PowerShell data. Use -Fragment for individual table sections, combine them with string concatenation, post-process with string replacement for row-level colors, and always write with -Encoding UTF8. The result is a report you can be proud to email to stakeholders.

Send-Item -To