21 Pure Tailwind2 Tailwind + JSPublished 1 new design · Updated on
23 Tailwind CSS dark mode components for SaaS dashboards, settings and login screens, modals, code editors, and reading layouts — spanning prefers-color-scheme theming, a data-theme override, a System / Light / Dark switcher, plus OLED black, mocha and navy palettes. Scoped under .tdm-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 23 Tailwind dark mode ui. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
The fix for the most-reported dark-mode bug: the split-second white flash on reload before your theme JS runs. A tiny blocking script placed first in <head> reads localStorage (falling back to the OS) and stamps data-theme on <html> before the first paint — and Tailwind's custom variant picks it up from there. Upgrade: the demo now proves it, measuring “theme set” against the browser's real first-paint time.
The baseline every other pattern builds on: the UI simply obeys the operating system. Each colour token is declared once in @theme as a light-dark() pair, and one utility — scheme-light-dark — hands the choice to the OS. Three panels render identical markup: a scheme-light reference, a scheme-dark reference, and a live panel that follows your system right now.
The layer you add when a visitor needs to disagree with their operating system. A data-theme attribute beats the OS, and a pure-CSS checkbox read by has-[#tdm-02-toggle:checked]: beats both. The CSS original needed four re-declared token blocks; here the whole cascade is six utilities on one element, including the flip-back rule that keeps the switch working for visitors whose OS already matches your default.
The architecture that stops a dark theme becoming forty hand-picked hex codes. Declare a three-stop primitive ramp once as light-dark() pairs, then derive every semantic token — surface, raised, hairline, muted ink, accent wash — inside @theme with color-mix(in oklab, …). Two cards pinned to opposite schemes prove the recipes adapt instead of inverting.
The modern way to write a theme, taken to its limit: no media query and no token block for the demo surface. Every colour on the review card is an arbitrary value — bg-[light-dark(#fff,#13151a)] — and the switch changes a single property, color-scheme, via scheme-* utilities. A locked panel pins scheme-only-light to prove one subtree can opt out.
A production navigation rail — brand lockup, six sections with live counters, a System / Light / Dark segmented control, settings and an account footer — that keeps its hierarchy in both themes because every surface, icon stroke and hairline is a token. The active item carries a filled surface and an accent edge drawn with aria-[current=page]:before:, so state never rests on colour alone.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Six metric cards — MRR, signups, churn, active users, response time, uptime — with SVG sparklines, tabular-figure readouts and delta chips that pair an arrow with colour so status never rests on hue alone. Cards sit on a radial wash between two surface tokens, which is what stops a dark dashboard reading as one flat black rectangle. Range selector and theme toggle, both pure CSS.
A real modal, with zero script: a native <dialog> opened by command="show-modal", so focus trapping, <kbd>Esc</kbd>, the top layer and inertness are all the browser's. Tailwind styles the scrim with backdrop:, animates entry with starting:open:, and the destructive button only arms once the workspace name is typed — enforced by pattern and group-has-[:valid].
A sign-in form where the focus indicator is the feature. Every control — email, password, reveal toggle, remember-me, both buttons and the recovery link — carries a 2px focus-visible:outline-tdm08-focus ring in a per-theme token measured at 3:1 or better. Proxied rings (has-focus-visible:) put the indicator on the visible element when the real input is hidden.
The control users actually expect: three states, not two. System adds nothing and lets prefers-color-scheme through; Light and Dark pin a scheme that survives an OS change mid-session. Three radios, one sliding thumb, zero JavaScript — plus a live truth table that highlights the exact cell (OS × choice) you are in right now.
An editor chrome — file tree, tab bar with an unsaved dot, gutter, code panel and status bar — where the syntax palette is a token set rather than a stylesheet per theme. Keyword, string, comment, function, number and property colours are light-dark() pairs, so the same highlighted markup reads correctly on a near-black editor and a paper-white one.
A product card for OLED panels, where an unlit pixel is genuinely off: the page is #000 and every layer above it climbs a short surface ramp — #0a0a0a, #141414, #1c1c1c — instead of relying on shadows a true-black substrate swallows. Finish swatches and a save toggle are pure-CSS state, and the card is pinned with scheme-only-dark so forced-dark tools leave it alone.
Maximum restraint: no borders, no shadows, no second accent — just surface separation, one hue and a lot of air. A preferences panel covering the four control types you actually ship (checkbox, switch, radio pair, select), all styled from three tokens. The Dark appearance switch is the page's theme control, and its knob follows the effective scheme, not just its checked state.
An editorial take on the confirmation dialog: deep navy rather than neutral black, warm off-white ink rather than pure white, an Instrument Serif headline over a sans body. The palette borrows from newspaper dark modes, where warmth keeps long-form text comfortable — and it turns a destructive action into something read rather than clicked through. Native <dialog>, opened and closed by invoker commands.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A terminal with prompt history, streamed output and a live input line — phosphor green on black, a 3% scanline overlay and a blinking block caret. The retro reference is deliberate, the contrast is not: the green measures 13:1 on the substrate, dim output still clears 6:1, and warnings carry a glyph as well as amber. Output lines stream in on load with starting: and staggered delays — no script.
Dark mode without the cold: roasted-brown surfaces, cream ink and a single burnt-orange accent, built as a focus writing surface with a title field, tag chips, word count and long-form copy. Light mode becomes a latte paper, not a white page. The ember only marks the caret, the selection, the active tags and the primary action — everything else is substrate, ink and one hairline.
A reading surface tuned for length: a green-tinted near-black that scatters less than pure black, ink at a deliberate 12:1 rather than a harsh 18:1, a 68-character measure, a drop cap and generous leading. A pure-CSS text-size picker writes one custom property; the hero image dims only when the effective theme is dark.
The elevation reference for a dark design system: four surface tokens, each a measured step lighter than the last, drawn as a visual ramp so the scale is legible at a glance. A release card holds three nested sub-cards, every level labelled with its token, and hover lifts a row one step — the way dark UIs signal depth when shadows can't.
A file browser in the dark: folder shelf, sortable column headers, alternating row tints derived with the opacity modifier rather than two hard-coded greys, and a selected row marked by an accent edge plus a checked box — never colour alone. A live "N selected" count is computed by CSS counters, so selection feedback works with zero script.
A changelog you would actually read: five releases alternating either side of a central rail, each in an elevated onyx card with a monospaced timestamp, a typed badge and a node that sits on the rail. The alternation is pure layout — md:odd: / md:even: — and collapses to a single left rail on phones. Cards rise in on load with starting:.
A product-launch card with a genuinely emissive CTA: the glow is a wide, low-alpha accent shadow-* layered under a tight inner highlight, so the button looks lit rather than blurred. A live countdown ticks in tabular figures (fifteen lines of script — the only JS in the collection, and never for theming), and the email field and button share one glowing frame that brightens on has-focus-visible:.
A launch hero with a real atmosphere: three offset radial gradients — green, violet, cyan — masked into the top of the panel so the aurora fades before it reaches the text, drifting slowly under a faint grid. Headline, subhead, dual CTAs and a trust bar, sized to fill a viewport at any width. The drift stops under motion-reduce:; the gradient stays.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The card that has to be read: a billing failure notice on a dark surface with a warm ember accent, a slow pulse on the status node and a retry action that is clearly the primary path. Severity is carried by an icon, a labelled chip and the copy itself — the ember is reinforcement, not the message. Service impact rows pair a dot with a written state.
FAQ
Frequently asked questions
How do I set up dark mode in Tailwind so it follows the operating system theme?
Let the OS drive it with no toggle at all. Declare your colour tokens as CSS custom properties on :root for the light values, then override those same properties inside an @media (prefers-color-scheme: dark) block. Every utility that reads a token — background, text, border — flips automatically when the visitor's system switches to dark, because the properties resolve to different values. In Tailwind v4 you point your @theme colours at the variables so bg-surface or text-ink stay semantic. The upside is zero JavaScript and no flash: the first paint already matches the OS. The limit is that the visitor can never disagree with their machine. If you need an in-page override, keep this media query as the baseline and layer a data-theme attribute on top of it, which is exactly what the toggle demos do.
What is the difference between prefers-color-scheme and a data-theme attribute toggle?
prefers-color-scheme is read-only: it reports what the operating system asked for and you cannot change it from the page. A data-theme attribute is a value you own — you write it to the <html> element and your CSS keys off [data-theme=dark]. The robust pattern uses both. The media query sets the default so the first paint is correct with no script, and the attribute, when present, wins over it so a visitor can force light or dark. Order matters: the attribute rules must be able to override the media-query rules, so either give them higher specificity or place them so the cascade favours them. A three-state switcher adds a System option that simply removes the attribute, handing control back to the media query. Persist the chosen value in localStorage and apply it before first paint to avoid a flash of the wrong theme.
How does the light-dark() function work and when can I use it instead of a media query?
light-dark() takes two values — light-dark(white, black) — and returns the first when the resolved colour scheme is light and the second when it is dark. For it to switch you must declare color-scheme: light dark on an ancestor, usually :root; without that the function has nothing to resolve against and always returns the light value. The appeal is that you write one declaration per property instead of a base rule plus a dark override block, so a whole component theme can live inline with no media query and no second token set. The trade-off is that it only responds to the resolved scheme. If you want a manual toggle you still set color-scheme from your data-theme attribute, or fall back to the token-override approach. Browser support is recent, so provide a plain colour fallback for older engines that ignore the function.
Why does my Tailwind dark mode toggle change nothing when my OS is already in dark mode?
This is the most common dark-mode bug. It happens when your toggle rule and your @media (prefers-color-scheme: dark) rule resolve to the same values. A visitor whose OS is set to dark already sees the media-query values on load; clicking the toggle swaps to the attribute rule, which carries identical colours, so nothing visibly moves. The fix is a mirror flip-back: inside the dark media query, add a rule for the toggled-to-light state that restores every light token, and inside the light media query do the reverse for the toggled-to-dark state. Restore all of the tokens, not just the background — missing one leaves half the palette stale. Test both directions on both OS settings. If the colour only flips for visitors whose system is on the opposite scheme to your default, you are missing this flip-back rule.
How do I build an accessible dark mode login form with visible focus indicators?
The failure mode on dark surfaces is a focus ring that blends into the background, so keyboard and screen-reader users lose their place. Drive every focusable control — email, password, the reveal toggle, the remember checkbox, the submit button — with :focus-visible rather than :focus, so the ring appears for keyboard navigation but not on mouse click. Give the ring real contrast against the surface, typically a bright accent plus an offset so it sits clear of the control edge; a dark ring on a dark field is invisible. Keep every input paired with a real <label>, and if you hide the label visually keep it in the DOM. The password reveal is a real <button> with aria-pressed so its state is announced, and it toggles the input type between password and text rather than restyling characters.
How do I get a true black OLED dark theme and should I use pure black everywhere?
A true-black theme sets the page to #000 so unlit OLED pixels are genuinely off, which saves power and gives the deepest possible contrast — the OLED product-card demo does exactly this. You do not paint everything black, though. Cards and controls sit on slightly lifted surfaces so the layout still reads; pure black on pure black has no edge. Pure black also has a cost for long-form reading: maximum contrast against white text increases halation, the smearing glow around glyphs, which tires the eye over paragraphs. That is why the blog reading layout uses a green-tinted near-black instead of #000, and the writing editor uses warm mocha-brown surfaces. Reserve true black for short, high-impact surfaces like a product card or a hero, and choose a soft near-black — tinted slightly cool, green or warm — for anything a visitor reads at length.
How should I build the surface and elevation layers for a dark design system?
In a dark UI you cannot lean on drop shadows to show depth the way light UIs do — a shadow on a near-black surface is barely visible. Depth instead comes from surface lightness: each layer that sits higher is a measured step lighter than the one beneath it. The elevation demo defines four surface tokens — base, raised, overlay, and the topmost — so a card on the page, a panel on the card, and a popover on the panel each read as a distinct plane. Define these as tokens, not ad-hoc hex values, so the steps stay consistent and you can tune the whole scale in one place. A clean trick for derived tints is to mix a translucent white over the base with color-mix() or an alpha overlay, which keeps every step on the same hue. Keep the steps small; if each layer jumps too far in lightness the interface looks striped rather than layered.
Can I open a modal dialog in dark mode without JavaScript, and is it accessible?
Yes. The modal demo uses the native <dialog> element opened declaratively — a button with command="show-modal" and commandfor pointing at the dialog's id, so no script wires up the open handler. Because it is a real <dialog> in modal state, the browser gives you the accessibility contract for free: focus moves into the dialog, focus is trapped while it is open, Escape closes it, and the rest of the page is marked inert so a screen reader cannot wander behind it. The dark-specific part is the backdrop — style the ::backdrop pseudo-element with a dark wash and a blur so the dimmed page reads as recessed rather than simply darker. Label the dialog with aria-labelledby pointing at its heading. The command attribute is recent; where it is unsupported you fall back to a small script calling showModal(), keeping the same markup.