Set the color scheme when you render, and style controls with ordinary CSS: OneRender draws native-looking controls in the light or dark scheme, applies :checked as a switch is clicked, and lets you replace a range slider’s track and thumb.
dotnet new console -o FormControls && cd FormControls dotnet add package RecursiveAI.OneRender.Image --version 1.3.1
Put this in Program.cs and run dotnet run:
using OneRender; using OneRender.Image; using OneRender.Interaction; using OneRender.Layout; using OneRender.Painting; using RecursiveAI.OneHtml; string html = """ <!doctype html> <html lang="en"> <style> :root { color-scheme: light dark; accent-color: #16a34a } body { margin: 0; padding: 16px; font: 15px sans-serif } label { display: flex; align-items: center; gap: 8px; margin: 0 0 12px } .switch { appearance: none; margin: 0; width: 36px; height: 20px; border-radius: 10px; background: #9ca3af } .switch:checked { background: #16a34a } .switch:checked + span { font-weight: bold } input[type=range] { appearance: none; width: 200px } input[type=range]::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: light-dark(#d1d5db, #4b5563) } input[type=range]::-webkit-slider-thumb { appearance: none; width: 18px; height: 18px; margin-top: -6px; border-radius: 50%; background: #16a34a } </style> <label><input type="checkbox" class="switch" id="sync"><span>Sync</span></label> <label><input type="checkbox" checked> Newsletter</label> <label>Volume <input type="range" id="volume" min="0" max="10" value="4"></label> <input type="text" placeholder="Search"> </html> """; // The host says which scheme the user prefers; `color-scheme: light dark` follows it. foreach (bool dark in new[] { false, true }) { RgbaImage shot = HtmlRender.ToImage(html, new HtmlRenderOptions { Width = 320, Height = 180, PrefersDarkColorScheme = dark }); File.WriteAllBytes(dark ? "dark.png" : "light.png", shot.ToPng()); Console.WriteLine($"{(dark ? "dark " : "light")}: canvas {Hex(shot, 300, 170)}"); } // Live: clicking the switch restyles it through `:checked`, and a drag moves the styled thumb. InteractivePage ui = InteractivePage.Create(html, new HtmlPageOptions(FontProviders.CreateDefault()) { ViewportWidth = 320, ViewportHeight = 180, PrefersDarkColorScheme = true }); Node sync = ui.FindElement("sync"); Node volume = ui.FindElement("volume"); ui.Click(sync, time: 16); Rect r = ui.Layout.GetBoundingClientRect(volume); double y = r.Y + r.Height / 2; ui.HandlePointer(new PointerInput { Action = PointerAction.Down, X = r.X + 20, Y = y, Button = 0, Buttons = MouseButtons.Primary, Time = 32 }); ui.HandlePointer(new PointerInput { Action = PointerAction.Move, X = r.X + r.Width * 0.8, Y = y, Buttons = MouseButtons.Primary, Time = 48 }); ui.HandlePointer(new PointerInput { Action = PointerAction.Up, X = r.X + r.Width * 0.8, Y = y, Button = 0, Time = 64 }); Console.WriteLine($"sync checked: {ui.IsChecked(sync)}, volume: {ui.GetRangeValue(volume)}"); var list = new DisplayList(); ui.Page.Paint(list); RgbaImage after = DisplayListRasterizer.Render(list, 320, 180); File.WriteAllBytes("after.png", after.ToPng()); Rect s = ui.Layout.GetBoundingClientRect(sync); Console.WriteLine($"switch: {Hex(after, (int)(s.X + 4), (int)(s.Y + s.Height / 2))}"); static string Hex(RgbaImage img, int x, int y) { int i = (y * img.Width + x) * 4; return $"#{img.Pixels[i]:x2}{img.Pixels[i + 1]:x2}{img.Pixels[i + 2]:x2}"; }
light: canvas #ffffff dark : canvas #121212 sync checked: True, volume: 8 switch: #16a34a
light.png and dark.png are the same page under each scheme: white or #121212 canvas, the field, checkbox and text in Chrome’s light or dark palette, and the slider track from light-dark(). In after.png the switch is green, Sync is bold through :checked + span, and the thumb sits at 8.
How it works
- The page opts in, the host decides.
color-scheme: light darkuses dark whenPrefersDarkColorSchemeis set onHtmlRenderOptionsorHtmlPageOptions, which also drives@media (prefers-color-scheme: dark). A page withoutcolor-schemestays light. System colors (Canvas,CanvasText,Field,ButtonFace,GrayText…) andlight-dark()resolve under the element’s scheme. - Native controls follow Chrome. Fields, buttons, selects, checkboxes, radios, range, progress and meter use Chrome’s light or dark look.
accent-colorsets the fill, and the check mark turns white or dark by contrast, as in Chrome.::placeholdersets the placeholder color, and::selectionthe selection highlight in a text field. - Form-state selectors stay live.
:checked,:indeterminate,:disabled,:enabled,:read-onlyand:read-write, with:checked + labeland:has(:checked), restyle after a click, a key,SetChecked,SetIndeterminateor a form reset, soappearance: nonecheckboxes and radios can be styled entirely in CSS. The restyle allocates nothing once warm. - A range is styled through its parts.
::-webkit-slider-runnable-trackand::-webkit-slider-thumb(and the-moz-names) set size, background, border, radius, shadow and margin, including themargin-topcentering trick.appearance: noneswitches from the native look to your boxes, and a drag follows the thumb as laid out.GetRangeValueandSetRangeValueread and set the value.
Every color here was measured from Chrome 156; the tables are in Forms and text editing.