Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker/configurator/variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ const standardVariables = {
WITH_CREDENTIALS: {
type: "boolean",
name: "withCredentials",
},
THEME_DEFAULT_MODE: {
type: "string",
name: "theme.defaultMode",
}
}

Expand Down
18 changes: 18 additions & 0 deletions docs/usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ Parameter name | Docker variable | Description
'none' (expands nothing).
</td>
</tr>
<tr>
<td><a name="user-content-theme"></a><code>theme</code>
</td>
<td><em>Unavailable</em></td>
<td><code>Object={ defaultMode: "light" }</code>. Theme configuration object.
</td>
</tr>
<tr>
<td><a name="user-content-theme.defaultmode"></a><code>theme.defaultMode</code>
</td>
<td><code>THEME_DEFAULT_MODE</code></td>
<td><code>String=["light"*, "dark"]</code>. Sets the default color mode
when Swagger UI loads. Can be <code>"light"</code> or <code>"dark"</code>.
This works across all layouts and deployment scenarios, including
embedded usage and swagger-ui-react. The mode can still be toggled
via the UI if a dark mode toggle is available in your layout.
</td>
</tr>
<tr>
<td><a name="user-content-filter"></a><code>filter</code></td>
<td><code>FILTER</code></td>
Expand Down
161 changes: 161 additions & 0 deletions docs/usage/dark-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# Dark Mode

Swagger UI supports dark mode to provide a better viewing experience in low-light environments. Dark mode can be enabled in several ways depending on your deployment scenario.

## Enabling Dark Mode via Configuration

The easiest way to enable dark mode is through the `theme` configuration option:

```javascript
SwaggerUI({
dom_id: '#swagger-ui',
url: 'https://petstore.swagger.io/v2/swagger.json',
theme: {
defaultMode: 'dark'
}
});
```

When `theme.defaultMode` is set to `'dark'`, Swagger UI will load in dark mode by default. This works across all layouts and deployment scenarios.

## Using the Dark Mode Toggle

If you're using the StandaloneLayoutPreset, a dark mode toggle button is available in the top bar. Users can click this button to switch between light and dark modes at any time.

```javascript
SwaggerUI({
dom_id: '#swagger-ui',
url: 'https://petstore.swagger.io/v2/swagger.json',
presets: [SwaggerUI.presets.apis, SwaggerUIStandalonePreset]
});
```

## System Preference Detection

When `theme.defaultMode` is not explicitly set to `'dark'`, Swagger UI will check the browser's system preference (`prefers-color-scheme: dark`) and automatically enable dark mode if the user's system is set to dark mode.

## Embedded Usage

Dark mode works seamlessly in embedded scenarios, including iframes and custom integrations:

```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>

<script src="./swagger-ui-bundle.js"></script>
<script>
SwaggerUI({
dom_id: '#swagger-ui',
url: 'https://petstore.swagger.io/v2/swagger.json',
theme: {
defaultMode: 'dark' // Enable dark mode by default
}
});
</script>
</body>
</html>
```

The dark mode styles are scoped to the `.swagger-ui` container, ensuring they don't interfere with your host application's styles.

## Using with swagger-ui-react

If you're using the React component version, you can enable dark mode via the `theme` prop:

```jsx
import SwaggerUI from "swagger-ui-react"
import "swagger-ui-react/swagger-ui.css"

function MySwaggerUI() {
return (
<SwaggerUI
url="https://petstore.swagger.io/v2/swagger.json"
theme={{ defaultMode: 'dark' }}
/>
)
}
```

## Programmatic Control

You can programmatically control dark mode using the dark mode plugin actions:

```javascript
const ui = SwaggerUI({
dom_id: '#swagger-ui',
url: 'https://petstore.swagger.io/v2/swagger.json'
});

// Enable dark mode
ui.darkModeActions.setDarkMode(true);

// Toggle dark mode
ui.darkModeActions.toggleDarkMode();

// Check if dark mode is enabled
const isDarkMode = ui.darkModeSelectors.isDarkMode();
```

## Docker Environment Variable

When using the Docker image, you can set the `THEME_DEFAULT_MODE` environment variable:

```bash
docker run -p 80:8080 -e THEME_DEFAULT_MODE="dark" swaggerapi/swagger-ui
```

## CSS Scoping and Customization

All dark mode styles are scoped to the `.swagger-ui` container within the `html.dark-mode` selector. This ensures:

- Dark mode styles only apply to Swagger UI elements
- No CSS leakage into your host application
- Easy customization through CSS overrides

### Custom Dark Mode Colors

You can customize dark mode colors by overriding the CSS variables or targeting specific selectors:

```css
/* Override dark mode background */
html.dark-mode .swagger-ui {
background: #1a1a1a;
}

/* Override dark mode text color */
html.dark-mode .swagger-ui {
color: #e0e0e0;
}
```

## Migration from Manual Class Manipulation

If you were previously adding the `dark-mode` class to the `<html>` element manually, you should migrate to using the `theme.defaultMode` configuration:

**Before:**
```javascript
SwaggerUI({
dom_id: '#swagger-ui',
url: 'https://petstore.swagger.io/v2/swagger.json'
});
// Manually add dark mode
document.documentElement.classList.add('dark-mode');
```

**After:**
```javascript
SwaggerUI({
dom_id: '#swagger-ui',
url: 'https://petstore.swagger.io/v2/swagger.json',
theme: {
defaultMode: 'dark' // Use configuration instead
}
});
```

The manual approach will continue to work for backwards compatibility, but using the configuration option is recommended as it integrates with the dark mode state management system.
3 changes: 3 additions & 0 deletions src/core/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const defaultOptions = Object.freeze({
configUrl: null,
layout: "BaseLayout",
docExpansion: "list",
theme: {
defaultMode: "system",
},
maxDisplayedTags: -1,
filter: false,
validatorUrl: "https://validator.swagger.io/validator",
Expand Down
8 changes: 8 additions & 0 deletions src/core/config/type-cast/mappings.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const mappings = {
typeCaster: numberTypeCaster,
defaultValue: defaultOptions.defaultModelsExpandDepth,
},
theme: {
typeCaster: objectTypeCaster,
defaultValue: defaultOptions.theme,
},
"theme.defaultMode": {
typeCaster: stringTypeCaster,
defaultValue: defaultOptions.theme.defaultMode,
},
displayOperationId: {
typeCaster: booleanTypeCaster,
defaultValue: defaultOptions.displayOperationId,
Expand Down
2 changes: 2 additions & 0 deletions src/core/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import DownloadUrlPlugin from "./plugins/download-url"
import SyntaxHighlightingPlugin from "core/plugins/syntax-highlighting"
import VersionsPlugin from "core/plugins/versions"
import SafeRenderPlugin from "./plugins/safe-render"
import DarkModePlugin from "./plugins/dark-mode"

import {
defaultOptions,
Expand Down Expand Up @@ -169,6 +170,7 @@ SwaggerUI.plugins = {
SyntaxHighlighting: SyntaxHighlightingPlugin,
Versions: VersionsPlugin,
SafeRender: SafeRenderPlugin,
DarkMode: DarkModePlugin,
}

export default SwaggerUI
64 changes: 64 additions & 0 deletions src/core/plugins/dark-mode/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @prettier
*/

export const DARK_MODE_SET = "dark_mode_set"
export const DARK_MODE_TOGGLE = "dark_mode_toggle"

/**
* Set dark mode state
* @param {boolean} isDarkMode - Whether dark mode should be enabled
*/
export function setDarkMode(isDarkMode) {
return {
type: DARK_MODE_SET,
payload: isDarkMode,
}
}

/**
* Toggle dark mode on/off
*/
export function toggleDarkMode() {
return {
type: DARK_MODE_TOGGLE,
}
}

/**
* Initialize dark mode based on configuration and system preferences
*/
export function initializeDarkMode() {
return (system) => {
const { getConfigs, darkModeActions } = system

// Get configs - this should be called after configs are loaded
const configs = getConfigs()

const theme = configs?.theme || {}
const defaultMode = theme.defaultMode || "system"

let shouldEnableDarkMode = false

if (defaultMode === "dark") {
// Explicitly set to dark mode
shouldEnableDarkMode = true
} else if (defaultMode === "light") {
// Explicitly set to light mode
shouldEnableDarkMode = false
} else if (defaultMode === "system") {
// Check system preference
if (typeof window !== "undefined") {
const prefersDarkMode =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
shouldEnableDarkMode = prefersDarkMode
}
}

// Apply dark mode if needed
if (shouldEnableDarkMode) {
darkModeActions.setDarkMode(true)
}
}
}
47 changes: 47 additions & 0 deletions src/core/plugins/dark-mode/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @prettier
*/
import * as actions from "./actions"
import * as selectors from "./selectors"
import reducers, { initialState } from "./reducers"

export default function darkModePlugin() {
return {
statePlugins: {
darkMode: {
initialState,
reducers,
actions,
selectors,
},
configs: {
// Hook into the configs loaded action to initialize dark mode
},
},
fn: {
// This hook is called after the system is fully initialized
// and configs are loaded, making it the perfect place to initialize dark mode
opsFilter: (taggedOps, fn) => fn(taggedOps),
},
afterLoad(system) {
// Use requestAnimationFrame to defer until next frame
// This ensures configs are loaded before initialization
if (typeof window !== "undefined" && window.requestAnimationFrame) {
window.requestAnimationFrame(() => {
const { darkModeActions } = system
if (darkModeActions && darkModeActions.initializeDarkMode) {
darkModeActions.initializeDarkMode()
}
})
} else {
// Fallback for non-browser environments
setTimeout(() => {
const { darkModeActions } = system
if (darkModeActions && darkModeActions.initializeDarkMode) {
darkModeActions.initializeDarkMode()
}
}, 0)
}
},
}
}
44 changes: 44 additions & 0 deletions src/core/plugins/dark-mode/reducers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @prettier
*/
import { Map } from "immutable"
import { DARK_MODE_SET, DARK_MODE_TOGGLE } from "./actions"

const initialState = Map({
isDarkMode: false,
})

export default {
[DARK_MODE_SET]: (state, action) => {
const isDarkMode = action.payload

// Apply or remove the dark-mode class on the HTML element
if (typeof document !== "undefined") {
if (isDarkMode) {
document.documentElement.classList.add("dark-mode")
} else {
document.documentElement.classList.remove("dark-mode")
}
}

return state.set("isDarkMode", isDarkMode)
},

[DARK_MODE_TOGGLE]: (state) => {
const currentMode = state.get("isDarkMode")
const newMode = !currentMode

// Apply or remove the dark-mode class on the HTML element
if (typeof document !== "undefined") {
if (newMode) {
document.documentElement.classList.add("dark-mode")
} else {
document.documentElement.classList.remove("dark-mode")
}
}

return state.set("isDarkMode", newMode)
},
}

export { initialState }
Loading