Onyx is the C# UI anti-framework.
There's no shortage of UI frameworks for C#. WinForms, WPF, and Avalonia, MAUI and WinUI 3, and there are ports of Qt and GTK and Dear IMGUI too. What these all have in common is that at some level, they're procedural: Even if you write <Button> in XAML, you're still really just writing new Button() and hoping that it runs at the right time.
Onyx is different.
Why do developers love the web? It's not because they love distributed programming — distributed programming is hard. What developers love is that after thirty years of testing and refinement by everyone on the entire planet, HTML and CSS have become great languages to work in. Modern web standards coding on evergreen browsers is predictable, reliable, straightforward, and generally, it just works. HTML markup describes semantic relationships well, the HTML DOM is well-understood and fairly easy to manipulate, and CSS is like magic for describing appearances.
But to get all of those lovely things, you have to have a browser. (Or something like Electron, which is just a browser in a box.) You can't do HTML and CSS without a browser.
Until now.
Onyx is half of a browser. (Or at least a major part of a browser.) Onyx is a pure-.NET, managed-code library that eats web-standards HTML and CSS, constructs all the same data structures that a browser constructs, manages them using all the same algorithms that a browser manages, and renders them the same way a browser does.
Onyx is:
- HTML parsing
- DOM construction and management
- CSS parsing
- Selector querying
- Style evaluation
- Flow and layout
- Rendering
Onyx is not:
- An HTTP client.
- A JavaScript runtime.
- An application framework.
It's not a browser. It's just the part of the browser that you always wished wasn't part of the browser.
Importantly, each of its pieces can be used independently. Want an advanced, standards-compliant HTML parser for C#? You can use just that part of Onyx and ignore the rest. Onyx has no opinions, other than that you should be using HTML and CSS for your UI.
In fact, you can even use Onyx with other major UI frameworks. It doesn't care what it's rendering to — on a window of its own, on an area of an existing UI, or even just onto a bitmap.
Intrigued? Read on.
Onyx is currently in an "alpha" status. It is not yet production-ready! Many of its intended features exist, and much of its intended functionality is implemented, but not all of it is there, and not all of it is well-tested yet.
Current implementation status:
- HTML parser: 100%
- DOM: 100%
- DOM Linq APIs: 100%
- CSS parser: 100%
- Optimized selector IsMatch(): 100%
- Optimized selector Find(): 100%
- Optimized style locator: 100%
- Computed-style applicator: 100%
- Basic layout: 0%
- Float layouts: 0%
- Flexbox: 0%
- Tables: 0%
- Render pipeline: 0%
- SkiaSharp backend: 25%
- Windows backend: 50%
- Event management: 0%
Eventually this is Nuget packages. For now, build it yourself. (See Building Onyx below.)
Onyx itself has no third-party dependencies on anything other than .NET 8+. (Backports to older .NET versions are intended, but not yet implemented.)
The Onyx core DLL contains all of the algorithms and data structures. These require nothing and expect nothing.
Future DLLs will be added for rendering and/or integration with other UI frameworks. The SkiaSharp backend will be the preferred backend, but SkiaSharp will not be a mandatory dependency of Onyx itself.
The baseline measure for Onyx 1.0 is fully standards-compliant HTML 5 and CSS 2.1, with several useful CSS 3 extensions:
- Common selector extensions, like
:checked display: flexand the rest of flexboxborder-radiusbox-shadowandtext-shadowbox-sizinglinear-gradientandradial-gradient- Multiple background images
background-size
In short, it's all the things you'd need to make a modern UI: The goal of Onyx is to be able to replace many of the use cases of other UI frameworks with ordinary HTML and CSS, and ideally be so close to standards-compliance that code from major CSS libraries can be used in Onyx verbatim.
Long-term, full CSS 3 compatibility would be desirable, but for now, the goal is CSS 2.1, plus a few of the most desirable CSS 3 enhancements.
Onyx supports the full set of HTML 5 elements that comprise the body of a document. Just imagine that <body>...</body> surrounds your content, and write whatever else you want.
(Elements that belong to the head are effectively metadata, and Onyx will simply ignore them if you use them. You don't need a <title> tag if you're already writing new Window("My Application") anyway.)
Special parsing is provided, as expected for standards compliance, for certain elements:
<style>,<xmp>, and<script>are consumed verbatim<img>,<link>,<br>,<hr>,<input>, and<meta>automatically close
Complex recovery rules are provided for mismatched tags, and for missing tags, per HTML 5 parsing rules.
Errors and mismatches are ignored, per HTML 5 parsing rules, but they are recorded and emitted as warnings, so that if you make mistakes in your markup, you can fix them.
Onyx has no strong opinion on the presentation of most elements — it provides a default stylesheet that matches that of most browsers, and it invites you to load your own CSS to style whatever you need.
Selectors follow standard CSS specificity rules. All standard CSS 2.1 selectors are supported, including the > and + and ~ operators, and attribute selectors, and starts-with and ends-with and contains attribute selectors too. Selectors may be used to style any element.
Note that the default stylesheet provides default styles for a <row> element and a <column> element — more on this below.
(NOTE: :before and :after pseudo-element selectors don't work yet. Sorry. It's on the list!)
Each of these should work as defined by web standards. If they do not, please report an issue.
align-content,align-items,align-selfbackground,background-attachment,background-color,background-image,background-origin,background-position,background-repeat,background-sizeborder,border-color,border-*-color,border-style,border-*-style,border-width,border-*-width,border-radius,border-*-radiusbox-shadow,box-sizingborder-collapse,border-spacingcaption-side,clear,clip,color,content,counter-increment,counter-reset,cursordirection,displayempty-cellsflex,flex-basis,flex-direction,flex-flow,flex-grow,flex-shrink,flex-wrapfont,font-family,font-size,font-style,font-variant,font-weightfloatjustify-contentletter-spacing,line-height,list-style,list-style-image,list-style-position,list-style-typemargin,margin-*min-width,width,max-widthmin-height,height,max-heightopacity,order,orphans,overflow,overflow-x,overflow-youtline,outline-color,outline-offset,outline-style,outline-widthpadding,padding-*position,page-break-before,page-break-after,page-break-insidequotesresizetable-layout,text-align,text-decoration,text-shadow,text-transformunicode-bidivertical-align,visibilitywhite-space,widows,word-spacingz-index
Onyx's APIs are designed to mimic the DOM where reasonable, but we use standard .NET constructs where we can, like Dictionary<K, V> and List<T>.
Onyx has a lot of classes inside, but these are the core classes you're likely to care about the most:
DOM Tree
Document- The root of any tree of nodes (elements). Loosely equivalent to the JSdocumentobject, but has far less attached to it, and is not a singleton.Node- The base class of bothElementandTextNodeandCommentNodeand more, this is simply a part of a document tree. Note that unlike with the JS DOM, attributes do not inherit fromNode.TextNode- A leaf node that contains a string of text.ContainerNode- The abstract base class ofElementandDocument, this represents any node that can contain other nodes, and provides aChildNodescollection and add/removal mechanics.Element- Every layout area of a document is represented by an element, just like with the JS DOM.LeafElement- A special child class ofElementthat prohibits child nodes from being added. This is the abstract base class of objects likeImageElementandInputElementandButtonElement.
HTML Parsing
HtmlParser- This is a full, standards-compliant HTML parser. It usesHtmlLexerto read a sequence of tokens, and constructs aDocumentobject from them and returns it.HtmlLexer- The lexer eats HTML, both plain text and tags, and it returns the nextHtmlTokenin the document each timeNext()is called. Attributes are parsed, but tags are just returned as tags, not asElementobjects.HtmlToken- One of these objects is returned on each invocation of the lexer. It has aSourceLocationattached to it indicating which source file and line it came from. This class is immutable.SourceLocation- A representation of a location in a source file, including file name, line number, column, and length. This class is immutable.
CSS Parsing
CssParser- This is a full, standards-compliant CSS 2.1 parser (with some CSS 3 support). It usesCssLexerto read a sequence of tokens, and constructs aStylesheetobject from them and returns it. This can correctly handle/ignore bad property and value declarations.CssSelectorParser- This knows how to parse just a single selector into aSelectoror aCompoundSelectorobject.CssPropertyParser- This knows how to parse just a single CSS property declaration into aStylePropertyobject.CssLexer- The lexer eats CSS, and returns it as a sequence ofCssTokenobjects.
CSS Styles
Stylesheet- A set of zero or moreStyleRuleobjects, in declaration order.StyleRule- This is a pairing of aCompoundSelectorwith aStylePropertySet, representing a single "rule" in a stylesheet.StyleProperty- This is the abstract base class for a single CSS property declaration. Properties are strongly-typed, so this will actually be any of a hundred different classes or so, likeBorderColorPropertyorDisplayProperty. This class and all its descendants are immutable. Instances of this class have aSourceLocation, and they can beToString()ed back to CSS text equivalent to that which was parsed.StylePropertySet- A set of zero or moreStylePropertyobjects, optimized for reading, not modification. This class is immutable.ComputedStyle- A copy-on-write tree of data that represents the "final" computed style for any element. This class is immutable.StyleManager- This tracks whichStylesheetobjects are loaded for a givenDocument, and provides optimized lookup structures for styles.
Selectors
CompoundSelector- This is a collection of one or moreSelectorobjects (comma-separated). This class is immutable.Selector- This represents a single CSS-style selector, and provides an optimized matching engine to test elements against it. This contains one or moreSimpleSelectorobjects, separated byCombinatortokens like space and>and+and~. It has aSpecificitythat can be compared against theSpecificityof other selectors. This class is immutable.SimpleSelector- This matches an element by testing various properties and attributes on the element itself. It may have anElementName, and may have zero or moreSelectorFilterobjects. This class is immutable.SelectorFilter- This matches an element by testing various properties and attributes on the element itself, not including the element's name. This is an abstract base class, and will be one of several child types, likeSelectorFilterIdorSelectorFilterAttriborSelectorPseudoClass.
Selectors can also be invoked via methods on Document and Element, like Document.Find(string selector) and Element.IsMatch(string selector) and by IEnumerable<T> extensions, like IEnumerable<Element>.Where(string selector).
Miscellaneous Types
Measure- This is a pairing of aUnit(likeemorpx) with adoublevalue. It is a readonly struct type. This provides common operators like+and*and==and<.MessageandMessages- AMessagescollection holds errors and warnings from different kinds of parsers, in the form ofMessageobjects, each of which have aKind, aSourceLocation, and theTextof the message itself.Messageis an immutable type.Messagesis thread-safe.HtmlEntities- This provides complete mapping tables for all known HTML&entity;declarations, and conversion methods.StringExtensions- This provides methods to convert HTML entities (HtmlEncode()andHtmlDecode()) and C-style backslash escapes (AddCSlashes()andStripCSlashes()) to plain text, and vice-versa. This also provides functions to convertTitleCaseidentifiers tohy-phen-izedidentifiers (Hyphenize()andTitleize()).
Classes in Onyx are not thread safe. Do not manipulate an object in Onyx from multiple threads at once. Bad things will happen.
However, classes don't have a thread affinity either — there's no such thing as "the UI thread." It's your responsibility to ensure that a Document tree isn't being stomped on by multiple threads at once, but Document and everything under it can safely be owned by any thread — and ownership can be transferred to a thread that the object wasn't created on. The same threading rules apply to Onyx classes that apply to something like Dictionary<K, V> or List<T>: It doesn't matter which thread modifies the object tree as long as it's only one thread at a time.
Future versions of Onyx may offer enhanced thread-safety to allow more flexibility in the "do not share objects" rule, but right now, assume all objects are not thread safe.
Note that many Onyx classes are immutable, and the immutable classes are therefore thread-safe. A Stylesheet, for example, is an immutable object, and can be safely shared across threads or tasks.
Onyx targets compatibility with HTML and CSS, but it explicitly does not target compatibility with the traditional DOM. Onyx's DOM is similar, but is more streamlined. Some legacy APIs have been removed, and new APIs have been added, and Onyx goes out of its way to provide Linq compatibility for easy element analysis and traversal.
Major differences include:
- Everything uses
TitleCase, per .NET naming conventions. GetElementById()andByClassName()andByType()do not return "live" element lists, but simply fixed collections.QuerySelectorAll()has been replaced with the far shorter nameFind(). There is noQuerySelector()equivalent.- Elements have no
GetAttributeBy()/SetAttribute()methods; they just have anAttributesproperty that implementsIDictionary<string, Attribute>. - Any
IEnumerable<Element>has many useful query methods, likeFind(selector)andWhere(selector)andClosest(selector)andDescendants()andAncestors(), often obviating the need for usingChildNodes/ParentNode/NextSibling/PreviousSiblingdirectly. - The
Classnameproperty is still whitespace-separated, but there is also aClassnamesproperty that is anIReadOnlySet<string>, andAddClass()andRemoveClass()andHasClass()are first-class methods on individualElements— and on collections. Documentis intentionally a simple container — unlike in the normal DOM, it's not very special or very complex or even necessarily the root of the element tree.Documentis aContainerNode, so it can host more than one child, not just a<body>element.DocumentFragmentis justDocumentminus all fast-selector-lookup logic.- Methods that return elements rarely return them in document order, for performance reasons: If you actually need document order for any collection of elements, there is an
.OrderByPosition()extension method onIEnumerable<Element>.
All of these differences are designed to simplify working with the DOM in modern C# by stripping away old cruft, adding methods inspired by jQuery, and ensuring deep Linq compatibility — in many cases, these changes allow complicated JavaScript DOM manipulations to be reduced to simple Linq one-liners.
Full compatibility with the traditional JS DOM is not in scope, nor is it expected to ever be in scope. Please do not report issues about missing DOM APIs.
Because Onyx is intended to be used for building UIs, we provide one very small tweak to HTML — still web-compatible, but designed to be useful for Onyx's target use case. The Onyx default stylesheet includes these simple rules:
row {
display: flex;
flex-flow: row nowrap;
}
column {
display: flex;
flex-flow: column nowrap;
}This effectively "creates" new <row> and <column> elements that don't exist in "normal" HTML by simply assigning them default flexbox styles and behaviors. You can reproduce the same effect in "normal" HTML by including the eight lines above on any web page, so we do not consider this to fundamentally be a breaking change with HTML and CSS.
You will need Visual Studio 2022 or later. Open the .sln file. Click "Build" > "Rebuild Solution". There should be neither warnings nor errors. The build will produce a single Onyx.dll as output.
Onyx is covered under the MIT Open-Source License. Share and enjoy.