Get started
Pick a framework once; every page on this site remembers it.
For the smallest bundle, import elements one by one and register each: import { defineCustomElement } from "@shineui/core/components/sui-button.js", then call defineCustomElement(). Bundlers has webpack, Next.js, Nuxt, the Angular CLI and a page with no build step.
Props and events are typed. No Vite config is needed. Keep @shineui/react and @shineui/core at the same version.
Form controls support v-model. No isCustomElement config is needed. Keep @shineui/vue and @shineui/core at the same version.
Write named slots as slot="footer" on the child; <template #footer> renders nothing and gives no warning. A ref holds the Vue component, so element methods are on ref.value.$el. The Vue tab of Frameworks has the rest.
[(ngModel)] and reactive forms work through the value accessors. Import the accessor beside the element, as above: a standalone component only sees directives in its own imports. Without it every binding throws NG01203, "No value accessor for form control".
There are seven accessors, one per kind of value; VALUE_ACCESSORS is all seven in one array. No CUSTOM_ELEMENTS_SCHEMA is needed. Works with Angular 20, 21 and 22. Keep @shineui/angular and @shineui/core at the same version. The Angular tab of Frameworks names each accessor.
The core sheet carries tokens and base rules and fetches nothing from the network. Every glyph an element draws itself (chevrons, ticks, spinners, arrows, close controls, the calendar glyph) is inline SVG, so the core sheet and the loader are enough. The fonts sheet is optional: it loads the DM Sans woff2 that ships beside it and requests nothing from another host. Without it the elements use the system sans. Font Awesome is needed only where you pass a Font Awesome class to an icon prop (icon on a button or a chip, an item's icon field on a menu or a tree, a column's icon); the element hands the class to an <i> and draws nothing for it itself. No ShineUI file loads Font Awesome. Install @fortawesome/fontawesome-free and serve its sheet and fonts from your own origin, so your content security policy needs no other host.
Three tags and no build step. The URLs name the major version, which the CDN resolves to the newest 1.x release, and the loader fetches its chunks from that same release, because the chunk names belong to the build that wrote the loader. Write an exact version, such as @1.2.0, in all three to choose when a page moves to a new release.
Under a content security policy, allow the CDN host in script-src, style-src and font-src, and set the nonce described below.
Every element writes its stylesheet into a <style> element when it first renders. A style-src without 'unsafe-inline' refuses those styles and every element renders bare. Generate a nonce per response, put it in the header, and pass the same value to setNonce before the first element renders. Import setNonce from the entry you load elements from: @shineui/core/loader for the lazy build, @shineui/core/components for the custom elements build and the three bindings, @shineui/ssr/components for a hydrating page. There is no root export; each entry carries its own runtime. Scripts need no nonce: the loader and the element chunks are files served under 'self'. A <meta name="csp-nonce" content="…"> in the head does the same as the call, and the theme sheet applyTheme writes reads it too. Security has the whole policy, Trusted Types, and what else your page has to filter.
Call setNonce before defineCustomElements(), or before the first component mounts through a binding. An element that has already rendered keeps its refused style element.
Five things the library depends on set the floor. The build targets ES2022; there is no ES5 fallback.
| What | Why it is load bearing | Needs |
|---|---|---|
The top layer (popover) | every floating surface paints above the page from where it sits in the DOM, so no ancestor can clip it or bury it under its stacking order | Chrome 114, Edge 114, Firefox 125, Safari 17 |
:has() | a scroller, a code block and a terminal draw their focus ring from the focused part inside them, and the data grid lifts its fill handle and its edit messages above the next row | Chrome 105, Edge 105, Firefox 121, Safari 15.4 |
color-mix() | the token sheet and the component stylesheets derive hover, soft and border tints from a base token | Chrome 111, Edge 111, Safari 16.2, Firefox 113 |
| Form-associated custom elements | the form controls post through ElementInternals, so a plain <form>, v-model and ngModel work without a shim | Chrome 77, Firefox 93, Safari 16.4 |
@layer | the token sheet, the semantic map and the preset maps are ordered by cascade layer | Chrome 99, Firefox 97, Safari 15.4 |
The floor: Chrome and Edge 114, Firefox 125, Safari 17, and iOS 17. Five things are progressive enhancement, and each degrades to something correct.
| What | Where | Needs | Without it |
|---|---|---|---|
:dir(rtl) | a glyph or a corner mirrored under right-to-left | Chrome 120, Firefox 49, Safari 16.4 | the LTR glyph or corner is drawn; layout still mirrors, because that is logical properties |
text-wrap: pretty | description paragraphs | Chrome 117, Safari 26; not in Firefox | ordinary wrapping |
text-wrap: balance | tooltip text and a chart tooltip's title and labels | Chrome 114, Firefox 121, Safari 17.5 | ordinary wrapping |
lh unit | empty-message heights | Chrome 109, Firefox 120, Safari 16.4 | the message sizes to its text |
scrollbar-width, scrollbar-color | the thin scrollbar on every scroller and the navy variant | Chrome 121, Firefox 64, Safari 26.2 (scrollbar-width from 18.2) | the platform scrollbar |
Windows High Contrast (forced-colors: active) is supported: every control keeps its shape and every state its reading.
JetBrains IDEs read web-types.json from the package on their own. VS Code needs one setting: "html.customData": ["./node_modules/@shineui/core/dist/vscode-data.json"]. dist/custom-elements.json is a Custom Elements Manifest for tooling and agents; dist/docs/shineui-docs.json is the full API as one document.
Every token is a CSS custom property in the global sheet. Presets and dark mode are attributes: data-sui-preset and data-sui-theme="dark". --danger defaults to orange-deep (ShineUI) or accent-deep (named presets); data-sui-status="semantic" remaps it onto --alert. sui-theme-picker writes those attributes and builds a sheet for any other pair. Theming lists every token with its value and offers the stylesheet as a download.
Every theme, shipped or built in the picker, meets WCAG 2.1 AA as it stands, with no setting to turn on. Accessibility has the figures.
From code, applyTheme in @shineui/core/theme writes the attributes and the sheet from a primary and an accent, and follows the operating system with mode: "system"; see Theming from code.
- Events. Every value event is a
sui-prefixed custom event:suiChange,suiSelect,suiInput. React surfaces it asonSuiChange, Vue as@sui-change, Angular as(suiChange). The payload is onevent.detail. Native events stay native: a button click isclick. - Complex data. Arrays, objects and functions (
columns,rows,options) are element properties, never attributes. The bindings pass them as ordinary props. - Slots. The default slot takes the body; named content carries
slot="footer". Each API table lists the names. In Vue too:<template #footer>renders nothing. - Forms. Inputs are form-associated custom elements: they post with plain forms, the Angular package ships ControlValueAccessors and Vue gets
v-model. - Overlays. Every floating surface, a dropdown, a menu, a popover, a dialog or a drawer, stays where it is in the DOM and paints in the browser's top layer (
popover="manual"), so a transformed, clipping or low z-index ancestor cannot clip it or bury it. Where the browser has no popover API it falls back toposition: fixed. Every surface closes on Escape and emitssuiClose. - Modals. While a dialog or a drawer is modal, it traps focus, and the rest of the page is inert and does not scroll, as it is behind a native modal
<dialog>. Toasts, tooltips, the modal's own popups and live regions stay usable. Mark an overlay of your own that must stay usable during a modal with thedata-sui-keep-liveattribute: it keeps keyboard and screen reader access, and a pointer reaches it when it paints above the modal's scrim, as a top-layer popover does. Accessibility has the details. - Flash of undefined content. Add
:not(:defined) { visibility: hidden; }or import the loader before render. - TypeScript. All from
@shineui/core. Every tag is inHTMLElementTagNameMap, sodocument.querySelector("sui-select")is anHTMLSuiSelectElement, and the class is exported by that name for aref. Data shapes are exported interfaces:SuiDataTableColumn,SuiSelectEntry,SuiCloseReason. The bindings carry the same types, so a wrong prop or a mistypeddetailis a compile error in React, Vue and Angular.
@shineui/core: the elements.@shineui/core/loaderregisters everything lazily;@shineui/core/components/sui-button.jsexports one element and thedefineCustomElement()that registers it;@shineui/core/shineui.cssand@shineui/core/shineui-fonts.cssare the sheets;@shineui/coreitself exports the types and the utilities (toast(),money(),trapFocus(),keyFilter()).@shineui/react,@shineui/vue,@shineui/angular: the bindings, typed. Install a binding and@shineui/coreat the same version: each binding pins the core it was built from, so a range that resolves the core one patch ahead ships two copies.
Every word the library ships is English by default and can be replaced. Call setLocale once to name the language and registerLocale to give it the words. Packs for eight languages ship with the package. Localisation has the packs and the key table.
Optional, in its own package. @shineui/ssr renders the markup on the server; @shineui/ssr/components is the same elements compiled with hydration. Server rendering has both.
A button costs about 13 KB gzip and a ten-element screen about 59 KB. Import elements one by one and a page pays only for the ones it renders.
Versioning
The five packages share one version number and follow semantic versioning, with the line drawn where your app feels a change:
- Major. A prop, event, method or slot is renamed or removed, an event's detail changes shape, a default changes what a page renders, or the browser floor moves.
- Minor. A new element, a new prop whose default keeps today's behavior, a new event, slot or token.
- Patch. A fix that makes an element do what it already said it did.
A token's name is part of the public API, because your own stylesheet can use it. Its value is not: restyling an element is a patch.
Stability
Every element is stable or in preview, and its page and its tile in the catalog say which. A stable element follows the rules above. A preview element works and is tested, but its API may change in a minor release.
An element is stable when all five of these hold:
- Its API is settled. It follows every rule in the conventions: names, booleans that default to false, attributes that reflect, one detail type per event name. No open design question names it.
- Tests pin its behavior. Unit specs render it and cover its props, events and methods. Where its main job depends on the browser itself, such as editing text in place, formatting under the caret or drag and drop, a browser test covers that too.
- Its keyboard is written down. Every key it handles is in the keyboard table on its page.
- It renders on the server.
@shineui/ssrdraws its markup with its data, so a server-rendered page does not wait for script to show it. - Its page is complete. An About of 60 words or fewer, and at least three examples.
An element leaves preview once all five hold.
All 146 elements are stable; none is in preview.