Note: in order to keep the page as concise as possible, IN some places I have posted the source code that best reflects the content of the article, leaving out other repetitive code, so if you try to read the source code yourself, you may find discrepancies with the code in this article. You are Naive UI. github.com/pftom/naive…

Concise abstraction

Front-end developers are now almost inseparable from UI component libraries, such as Ant Design, Material Design and, more recently, Naive UI, which are emerging from the Vue ecosystem. Component libraries offer simple, flexible and easy-to-use forms of use. The most common use of a Button on a page is as follows:

<template> <n-button>Default</n-button> <n-button type="primary">Default</n-button> <n-button type="info" dashed>Default</n-button> <n-button type="success" dashed size="small">Default</n-button> <n-button Text > Default < / n - button > < n - the button text tag = "a" href = "https://anyway.fm/news.php" type = "warning" > AnNiWei times < / n - button > <n-button disabled> </n-button> </template>Copy the code

These simple lines of code can do something interesting:

You can even switch skins with one click, as in Dark Mode:

We can also handle events, add ICONS, handle Loading, and so on. By simply giving some Props, we can have a nice, functional Button that is nothing compared to the original HTML tag…

Iceberg theory

While component libraries bring flexibility and convenience, their internal principles are not as simple as they are to use, just like the iceberg diagram above.

Take a look at the latest Vue component library, Naive UI’s CHANGELOG, to get an idea of how much time it takes to write a beginner’s component library:

As you can see, the 1.x version was released on March 21, 2020-21, and there was a long period of thinking, design and development before 1.x, which is almost two years now.

To run a Naive UI Button, you need the following files or code:

. |_____utils | |____color | | |____index.js | |____vue | | |____index.js | | |____flatten.js | | |____call.js | | |____get-slot.js | |____index.js | |____naive | | |____warn.js | | |____index.js | |____cssr | | |____create-key.js | | |____index.js |_____internal | |____loading | | |____index.js | | |____src | | | |____Loading.jsx | | | |____styles | | | | |____index.cssr.js | |____index.js | |____icon-switch-transition | | |____index.js | | |____src | | | |____IconSwitchTransition.jsx | |____fade-in-expand-transition | | |____index.js | | |____src | | | |____FadeInExpandTransition.jsx | |____wave | | |____index.js | | |____src | | | |____Wave.jsx | | | |____styles | | | |  |____index.cssr.js | |____icon | | |____index.js | | |____src | | | |____Icon.jsx | | | |____styles | | | | |____index.cssr.js |_____styles | |____common | | |_____common.js | | |____light.js | | |____index.js | |____transitions  | | |____fade-in-width-expand.cssr.js | | |____icon-switch.cssr.js | |____global | | |____index.cssr.js |____config-provider | |____src | | |____ConfigProvider.js |____button | |____styles | | |_____common.js | | |____light.js | | |____index.js | |____src | | |____Button.jsx | | |____styles | | | |____button.cssr.js |____assets | |____logo.png |_____mixins | |____use-style.js | |____use-theme.js | |____index.js | |____use-form-item.js | |____use-config.jsCopy the code

Behind the seemingly difficult

Although running through a deceptively simple requires a lot of work, involving dozens of file dependencies, for a component library the complexity is approximately of an order of magnitude, from a simple to a complex

. In fact, within the domain of component libraries, 90% of the content is similar, so if you understand the flow of
, you can basically say that you understand nearly 90% of the content of the component library, and the remaining 10% is the concrete implementation of specific components.

Therefore, to understand the core of a front-end component library, it is necessary to understand the various preparations behind a run, which is the first column in the figure above, and the development of a component library should first focus on the design of at least one Button run solution.

The technical chain behind Button

We take Naive UI as the research object to analyze the various principles behind the implementation of in detail. There are two intuitive reasons:

  1. The stack is mainly Vite, Vue3 and TypeScript, which is consistent with the author’s recent stack
  2. Compared with other component libraries, it is at a relatively compromise level in maturity, popularity and code excellence. It is not too complex but involves relatively more knowledge, so it is more suitable for learning and studying its principles

Start from the template

The first thing you need to know about a component is its skeleton, which is also known as HTML/JSX content. See Naive UI’s Button component template:

const Button = defineComponent({ name: 'Button', props: }, setup(props) {}, render() {// n const {$slots, mergedClsPrefix, tag: Component} = this; const children = flatten(getSlot(this)); <Component ref="selfRef" class={[' ${mergedClsPrefix}-button ', `${mergedClsPrefix}-button--${this.type}-type`, { [`${mergedClsPrefix}-button--disabled`]: this.disabled, [`${mergedClsPrefix}-button--block`]: this.block, [`${mergedClsPrefix}-button--pressed`]: this.enterPressed, [`${mergedClsPrefix}-button--dashed`]: !this.text && this.dashed, [`${mergedClsPrefix}-button--color`]: this.color, [`${mergedClsPrefix}-button--ghost`]: this.ghost, // required for button group border collapse }, ]} tabindex={this.mergedFocusable ? Zero: -1} type={this.attrType} style={this.cssVars} disabled={this.disabled} onClick={this.handleClick} onBlur={this.handleBlur} onMousedown={this.handleMouseDown} onKeyup={this.handleKeyUp} onKeydown={this.handleKeyDown} > $slot.default && this.iconPlacement === "right"? (<div class={`${mergedClsPrefix}-button__content`}>{children}</div> ) : <NFadeInExpandTransition width> {{default: () => $slots.icon || this.loading ? ( <span class={`${mergedClsPrefix}-button__icon`} style={{ margin: ! $slots.default ? 0 : "", }} > <NIconSwitchTransition> {{ default: () => this.loading ? ( <NBaseLoading clsPrefix={mergedClsPrefix} key="loading" class={`${mergedClsPrefix}-icon-slot`} strokeWidth={20} /> ) : ( <div key="icon" class={`${mergedClsPrefix}-icon-slot`} role="none" > {renderSlot($slots, "icon")} </div> ), }} </NIconSwitchTransition> </span> ) : null, }} </NFadeInExpandTransition> // Section 3 {$slot.default && this.iconPlacement === "left"? (<span Class ={' ${mergedClsPrefix}-button__content '}>{children}</span>) : null} this.text ? (<NBaseWave ref="waveRef" clsPrefix={mergedClsPrefix} />) : null} ( <div aria-hidden class={`${mergedClsPrefix}-button__border`} style={this.customColorCssVars} /> ) : Null} // Part 6 {this.showborder? ( <div aria-hidden class={`${mergedClsPrefix}-button__state-border`} style={this.customColorCssVars} /> ) : null} </Component> ) } });Copy the code

As can be seen, the above mainly shows the template part of component. The component is defined based on Vue3 defineComponent, and the template is written in the form of JSX based on render method. The template part is divided into 6 parts. To comment out in code:

  1. It is mainly related to attributes, mainly including three attributes:$slotsmergedClsPrefixtag, including$slotsObjects like child nodes in the Vue domain,mergedClsPrefixIs the namespace prefix for the entire component library, which in Naive UI isntagWhat label should this component be presented with<button />You can also change it to<a />Make the button look like a link

  1. Define button-related attributes:

    1. Among themclassIt is determined by the attributes that are passed intype:primaryinfowarningsuccesserror, and what state it is in:disabledblockpresseddashedcolorghostAccording to thesetypeAnd state are given the appropriate class name to define the CSS style to which the component belongs
    2. tabIndex“Is in usetabKey, whether the button will be selected,0Means available for selection,- 1Indicates that it is not available.
    3. typeIs represented asbuttonsubmitresetButton type, so that the button can be integrated into<Form />Components to perform more complex operations, such as triggering a form submission;
    4. stylePass in the required CSS Variables for this component, that is, CSS Variables, while insetupFunction, will passuseTheme(more on this later) Hook to mountButtonThese styles use CSS Variables extensively to customize various CSS properties of components, as well as handle global theme switching, such as Dark Mode
    5. disabledIs to control whether the button can be operated,trueIndicates disabled, not operable,falseIndicates the default value
    6. The rest are the associated event handlers:clickblurmouseupkeyupkeydown

  1. The main decision is iniconPlacementleftright, component child node display form, that is, icon on the left and right, child node distribution to<span /><div />The form of the label is displayed when forrightIs set to<div />For better layout and positioning

  1. Is icon related content,NFadeInExpandTransitionTo control the transition animation of the Icon appearing and disappearing,NIconSwitchTransitionIs the controlloadingForm Icon and other Icon switch transition animation

  1. When the button does nottextIn the form of node display, there should be ripples on it to process feedback. It can also be seen from the above video that there will be corresponding ripple effect to give click feedback when the button is clicked. The figure below shows a text-like form, and ripple diffusion effect cannot appear when the button is clicked

  1. Mainly through<div />To simulate component borders:borderstate-border, the former is mainly static, default processing border color, width, etc., while the latter is processing in different states:focushoveractivepressedSuch as theborderstyle

Here’s a practical example of how both work:

.n-button .n-button__border {

    border: var(--border);

}



.n-button .n-button__border, .n-button .n-button__state-border {

    position: absolute;

    left: 0;

    top: 0;

    right: 0;

    bottom: 0;

    border-radius: inherit;

    transition: border-color .3s var(--bezier);

    pointer-events: none;

}



.n-button:not(.n-button--disabled):hover .n-button__state-border {

    border: var(--border-hover);

}



.n-button:not(.n-button--disabled):pressed .n-button__state-border {

    border: var(--border-pressed);

}



style attribute {

    --bezier: ;

    --bezier-ease-out: ;

    --border: 1px  ;

    --border-hover: 1px  ;

    --border-pressed: 1px  ;

    --border-focus: 1px  ;

    --border-disabled: 1px  ;

}
Copy the code

As you can see, the state border handles dynamic effects such as hover, pressed, etc. while the border handles the initial default effects.

Now that you know about the main template, you may be confused about the one that comes up most often in the entire template, which is:

  • ${mergedClsPrefix}-button
  • ${mergedClsPrefix}-button--${``this``.type}-type
  • ${mergedClsPrefix}-button__content
  • ${mergedClsPrefix}-button--disabled

Why is there such a strange CSS class notation? And when assigning attributes to components:

  • style``={``this``.cssVars}

A typical example is:

const cssVars = {

  // default type

  color: "#0000",

  colorHover: "#0000",

  colorPressed: "#0000",

  colorFocus: "#0000",

  colorDisabled: "#0000",

  textColor: textColor2,

}
Copy the code

Why do I need to assign a bunch of CSS Variables?

If you are puzzled by these questions and want to understand the principles behind them, then you should take a breath and keep focused on the next part of this article: the Art of organizing styles.

Style of organization art

In the component library space, most of the time has been spent on how to better and more customizable organize the overall style system.

Naive UI has an interesting feature. It does not use any pre-processing or post-processing style languages such as Less/Sass/PostCSS. Instead, it makes its own csS-in-JS solutions that are framework-based and framework-based independent with SSR features: Css-render, and give this program to design a set of plug-in system, there are two main plug-ins:

  • vue3-ssr
  • plugin-bem

This article focuses on CSR, so I will only focus on plugin-BEM.

Css-render the current basic use scenario for the use of plugin-BEM plug-in, write based on BEM style, easy to organize CSS in JS code, as for why here is the “class” CSS in JS solution, will be explained later.

After we have installed the corresponding package:

$ npm install --save-dev css-render @css-render/plugin-bem
Copy the code

It can be used as follows:

import CssRender from 'css-render' import bem from '@css-render/plugin-bem' const cssr = CssRender() const plugin = bem({ blockPrefix: '.ggl-'}) CSSR. Use (plugin) const {cB, cE, cM} = plugin ('container', [ cE( 'left, right', { width: '50%' } ), cM( 'dark', [ cE( 'left, right', { backgroundColor: 'black'})])]) console.log(style.render()) You can provide options style.mount(/* options */) // remove the mounted style style.unmount(/* options */)Copy the code

The effect of the above Log is as follows:

.ggl-container .ggl-container__left, 

.ggl-container .ggl-container__right {

  width: 50%;

}



.ggl-container.ggl-container--dark .ggl-container__left, 

.ggl-container.ggl-container--dark .ggl-container__right{

  background-color: black;

}
Copy the code

As you can see, the above code mainly uses cB, cE and cM functions for nested combinations of tags and styles to define canonical CSS classes and their corresponding styles. In order to further explain the function of this library and its effects in Naive UI, it is necessary to know what BEM is.

What is BEM?

B (Block), E (Element), and M (Modifier) — blocks, elements, and modifiers — are widely used naming conventions for classes used in HTML/CSS:

Block / * * / BTN {} * /. / * rely on block elements btn__price {}. Btn__text {} / * the state of the modified block modifier. * / BTN - orange {}. The BTN - big {}Copy the code
  • The above middle Block, i.ebtnIs used to represent an abstract top-level new component, that is, a block cannot contain a block, and is also treated as a parent node in a tree.btnsaid
  • Element, “Element,” i.epricetextIs a child element of the block, followed by the block, separated by a double underscore.btn__price.btn__textsaid
  • Modifier, that isorangebig, used to modify the state of a block, add a specific theme or style to the block, followed by a block, separated by a double hyphen, used by.btn--orange.btn--bigsaid

When the CSS form above is reflected in HTML, it should have the following structure:

<a class=" BTN BTN --big BTN --orange" href=""> <span class="btn__price">¥9.99</span> <span class="btn__text"> order </span> </a>Copy the code

This BEM style of class naming has several basic advantages:

  1. Can represent almost all elements and their dependencies, and the relationship is clear, semantic clear
  2. And even developers in other fields, such as client development, or designers, who don’t know CSS, can learn about elements, their hierarchical relationships, and states from this naming style
  3. With a similar naming structure, you can then change the class name slightly to get elements with different styles, such as buttons:
/* Block */

.btn {

  text-decoration: none;

  background-color: white;

  color: #888;

  border-radius: 5px;

  display: inline-block;

  margin: 10px;

  font-size: 18px;

  text-transform: uppercase;

  font-weight: 600;

  padding: 10px 5px;

}



/* Element */

.btn__price {

  background-color: white;

  color: #fff;

  padding-right: 12px;

  padding-left: 12px;

  margin-right: -10px; /* realign button text padding */

  font-weight: 600;

  background-color: #333;

  opacity: .4;

  border-radius: 5px 0 0 5px;

}



/* Element */

.btn__text {

  padding: 0 10px;

  border-radius: 0 5px 5px 0;

}



/* Modifier */

.btn--big {

  font-size: 28px;

  padding: 10px;

  font-weight: 400;

}



/* Modifier */

.btn--blue {

  border-color: #0074D9;

  color: white;

  background-color: #0074D9;

}



/* Modifier */

.btn--orange {

  border-color: #FF4136;

  color: white;

  background-color: #FF4136;

}



/* Modifier */

.btn--green {

  border-color: #3D9970;

  color: white;

  background-color: #3D9970;

}





body {

  font-family: "fira-sans-2", sans-serif;

  background-color: #ccc;

}
Copy the code

The above modifiers orange, green, blue, big, etc., can have different effects:

How does CSS Render work?

CSS Render is essentially a CSS generator, which then provides mount and unmount apis for mounting the generated CSS string to the HTML template and removing the CSS style tag from the HTML. It implements the Sass/Less/CSS-in-JS scheme with the help of the BEM naming convention plug-in and CSS Variables, reducing the repetitive logic and package size of the overall CSS.

With BEM and CSS Render in mind, let’s review the following code:

import CssRender from 'css-render' import bem from '@css-render/plugin-bem' const cssr = CssRender() const plugin = bem({ blockPrefix: '.ggl-'}) CSSR. Use (plugin) const {cB, cE, cM} = plugin ('container', [ cE( 'left, right', { width: '50%' } ), cM( 'dark', [ cE( 'left, right', { backgroundColor: 'black'})])]) console.log(style.render()) You can provide options style.mount(/* options */) // remove the mounted style style.unmount(/* options */)Copy the code

The above code does the following:

  1. Initialize the CSS Render instance, then initialize the BEM plug-in instance and prefix the overall style class with.ggl-

  2. From the BEM plug-in to export the relevant cB, cE, cM methods, and then based on these three methods to comply with the concept of BEM style class arrangement, nesting, combination to form our final style class and the corresponding style

    1. The first iscB, defines a top-level block element ascontainer
    2. Then the block contains two child elements, respectivelycERepresents the child element that belongs to the parent blockleftright, corresponding to aboutwidthThe style; As well ascMModifier that modifies the parent blockdark
    3. darkThe modifier in turn contains a child element that belongs tocERepresents belonging to the block that this modifier modifies and contains child elementsleftright, corresponding to aboutbackgroundColorThe style of the

Now that we know about the hierarchy nesting, we can render the style above:

//. GGL - prefix, and cB('container', [cE('left, right', {width: '50%' } )]) .ggl-container .ggl-container__left, .ggl-container .ggl-container__right { width: 50%; } //. GGL - prefix, and cB('container', [cM('dark', [cE('left, right', {backgroundColor: 'black' } )])]) .ggl-container.ggl-container--dark .ggl-container--left, .ggl-container.ggl-container--dark .ggl-container__right { background-color: black; }Copy the code

Gcl-container. Gcl-container –dark is written directly with the parent block’s class, and is a modifier, not a dependency.

Naive UI style organization

Naive UI mainly follows the following logic in style organization, again using Button as an example:

  • Mount CSS Variables, which contain default Variables and user-passed Variables from the definition, willcssVarstaggedstyleField to mount
  • Mount button-related base styles, theme-related styles, and generate CSS class names
  • Mount global default styles (at the end of this step, make sure global default styles are not overridden)

With the above three steps, you can define all the classes and styles associated with a Button, and support theme customization and theme reloading through CSS Variables.

Use the useTheme hook in the setup function to handle button-related style mounting and global default style mounting, and then handle CSS Variables definition and use:

const Button = defineComponent({ name: "Button", props: buttonProps, setup(props) { const themeRef = useTheme( "Button", "Button", style, buttonLight, props, mergedClsPrefixRef ); Return {// define border color related customColorCssVars: computed(() => {}), // define font, border, color, size related cssVars: computed(() => {}), }} render() {// define button-related CSS variables <Component style={this.cssvars}> // define border color unique CSS variables <div class={`${mergedClsPrefix}-button__border`} style={this. customColorCssVars} /> <div class={`${mergedClsPrefix}-button__state-border`} style={this. customColorCssVars} /> </Component> } });Copy the code

Mount buttn-related styles

UseTheme Hooks are used in the Button component setup method. UseTheme is a hook function with the following structure:

/* Global CSS Variables */ type ThemeCommonVars = typeof {primaryHover: '# 36ad6A ', errorHover: '#de576d',... } // Button's CSS Variable type ButtonThemeVars = ThemeCommonVars & {/* The type of button-related CSS Variables */} // Theme Interface Theme<N, T = {}, R = any> {// Theme name: N // Theme Some common CSS Variables common? : ThemeCommonVars // CSS Variables that depend on components, such as buttons in the Form. : R // Some personalized CSS Variables self for the theme itself? : (vars: ThemeCommonVars) => T} // Type ButtonTheme = Theme<'Button', ButtonThemeVars > interface GlobalThemeWithoutCommon { Button? : ButtonTheme Icon? Type UseThemeProps<T> = Readonly<{// Theme-related variables, such as darkTheme theme? : T | undefined / / theme of variables can be overloaded themeOverrides? : ExtractThemeOverrides<T> // builtinThemeOverrides, a variable in a built-in theme that can be overridden? Abstract: Theme<T> = T extends Theme<unknown, infer V, infer W>? { common: ThemeCommonVars self: V // CSS Variables for dependent components, such as buttons in Form, should be included with peers: W // Some CSS Variables related to dependent components, such as the Form relies on Button, corresponding to the Button // need to contain CSS Variables should be limited, The CSS in the Variables can be overloaded variable peerOverrides: ExtractMergedPeerOverrides < T >} : T useTheme < N, T, R > (resolveId: keyof GlobalThemeWithoutCommon, mountId: string, style: CNode | undefined, defaultTheme: Theme<N, T, R>, props: UseThemeProps<Theme<N, T, R>>, // n clsPrefixRef? : Ref<string | undefined> ) => ComputedRef<MergedTheme<Theme<N, T, R>>>Copy the code

As you can see, useTheme takes 6 arguments:

  • resolveIdUsed to locate the key value in the global style topic, here is'Button'
  • mountIdStyle mount toheadThe label,styleid
  • styleThe component’s CSS Render form generates style tags, style strings, which are button-related classes, and the skeleton of classes and styles. Inside are a series of CSS Variables to be used
  • defaultThemeButtonThe default theme related to CSS Variables
  • propsCustomizable attributes passed in for users to use components to override default style variables
  • clsPrefixRefIs the overall style class prefix. In Naive UI, this isn

UseTheme returns a collection of styles ComputedRef

>> combining the built-in styles, globally defined button-related styles, and user-defined styles.

Now that you know the input and output of the useTheme hook function, you can look at its body logic:

function useTheme( resolveId, mountId, style, defaultTheme, props, clsPrefixRef ) { if (style) { const mountStyle = () => { const clsPrefix = clsPrefixRef? .value; style.mount({ id: clsPrefix === undefined ? mountId : clsPrefix + mountId, head: true, props: { bPrefix: clsPrefix ? `.${clsPrefix}-` : undefined, }, }); globalStyle.mount({ id: "naive-ui/global", head: true, }); }; onBeforeMount(mountStyle); } const NConfigProvider = inject(configProviderInjectionKey, null); const mergedThemeRef = computed(() => { const { theme: { common: selfCommon, self, peers = {} } = {}, themeOverrides: selfOverrides = {}, builtinThemeOverrides: builtinOverrides = {}, } = props; const { common: selfCommonOverrides, peers: peersOverrides } = selfOverrides; const { common: globalCommon = undefined, common: globalSelfCommon = undefined, self: globalSelf = undefined, peers: globalPeers = {}, } = {}, } = NConfigProvider? .mergedThemeRef.value || {}; const { common: globalCommonOverrides = undefined, = {}, } = NConfigProvider? .mergedThemeOverridesRef.value || {}; const { common: globalSelfCommonOverrides, peers: globalPeersOverrides = {}, } = globalSelfOverrides; const mergedCommon = merge( {}, selfCommon || globalSelfCommon || globalCommon || defaultTheme.common, globalCommonOverrides, globalSelfCommonOverrides, selfCommonOverrides ); const mergedSelf = merge( // {}, executed every time, no need for empty obj (self || globalSelf || defaultTheme.self)? .(mergedCommon), builtinOverrides, globalSelfOverrides, selfOverrides ); return { common: mergedCommon, self: mergedSelf, peers: merge({}, defaultTheme.peers, globalPeers, peers), peerOverrides: merge({}, globalPeersOverrides, peersOverrides), }; }); return mergedThemeRef; }Copy the code

You can see that the useTheme body logic consists of two parts:

  • The first part is to mount button-related styles to clsPrefix + mountId, including the button-related style class skeleton, and to mount the global generic styles to naive- UI /global. This style is mounted on the onBeforeMount hook. This style is mounted in the onBeforeMount hook.

    • The ordersetupIt returns CSS Variables and passes the tagstyleRegistered CSS Variables
    • Then mount the style skeleton associated with the Button
    • Then mount the global generic style skeleton to ensure that the button-related style skeleton does not overwrite the global generic style
  • The second part generates a new set of theme variables for the integration of user-defined themes and internally configured themes

    • User-defined themeprops: containsthemethemeOverridesbuiltinThemeOverrides
    • Topic of internal configurationNConfigProvider? .mergedThemeRef.valueNConfigProvider? .mergedThemeOverridesRef.value

The following sections will focus on the specific code and the meaning of the related variables.

The button-related styles in Part 1 are as follows:

import { c, cB, cE, cM, cNotM } from ".. /.. /.. /_utils/cssr"; import fadeInWidthExpandTransition from ".. /.. /.. /_styles/transitions/fade-in-width-expand.cssr"; import iconSwitchTransition from ".. /.. /.. /_styles/transitions/icon-switch.cssr"; export default c([ cB( "button", ` font-weight: var(--font-weight); line-height: 1; font-family: inherit; padding: Var (--padding); //.... more definitions', [// border, border-related style class skeleton cM("color", [cE("border", {borderColor: "var(--border-color)", }), cM("disabled", [ cE("border", { borderColor: "var(--border-color-disabled)", }), ]), cNotM("disabled", [ c("&:focus", [ cE("state-border", { borderColor: "var(--border-color-focus)", }), ]), c("&:hover", [ cE("state-border", { borderColor: "var(--border-color-hover)", }), ]), c("&:active", [ cE("state-border", { borderColor: "var(--border-color-pressed)", }), ]), cM("pressed", [ cE("state-border", { borderColor: "Var (- border - color - pressed)",}),),),), / / icon style class skeleton cE (" icon ", ` margin: var (-- icon - margin); margin - left: 0; height: var(--icon-size); width: var(--icon-size); max-width: var(--icon-size); font-size: var(--icon-size); position: relative; flex-shrink: 0; `, [ cB( "icon-slot", ` height: var(--icon-size); width: var(--icon-size); position: absolute; left: 0; top: 50%; transform: translateY(-50%); display: flex; `, [ iconSwitchTransition({ top: "50%", originalTransform: "TranslateY (50%),"}),]), fadeInWidthExpandTransition (),]), / / the content > child element content related style class skeleton cE (" content ", ` display: flex; align-items: center; flex-wrap: nowrap; `, [ c("~", [ cE("icon", { margin: "var(--icon-margin)", marginRight: 0,}),]),]), // More about backgroundColor, base-wave click feedback ripple, icon, content, C ("@keyframes button-wave-spread", {from: {boxShadow: "0 0 0.5px 0 var(--ripple-color)",}, to: { // don't use exact 5px since chrome will display the animation with glitches boxShadow: 0 0 0.5px 4.5px var(-- rip-color)",},}), c("@keyframes button-wave-opacity", {from: {opacity: "var(--wave-opacity)", }, to: { opacity: 0, }, }), ]);Copy the code

The CSS Render related code above will eventually produce something of the following type:

.n-button { font-weight: var(--font-weight); line-height: 1; font-family: inherit; padding: var(--padding); transition: color .3s var(--bezier), background-color .3s var(--bezier), opacity .3s var(--bezier), border-color .3s var(--bezier); } .n-button.n-button--color .n-button__border { border-color: var(--border-color); } .n-button.n-button--color.n-button--disabled .n-button__border { border-color: var(--border-color-disabled); } .n-button.n-button--color:not(.n-button--disabled):focus .n-button__state-border { border-color: var(--border-color-focus); } .n-button .n-base-wave { pointer-events: none; top: 0; right: 0; bottom: 0; left: 0; animation-iteration-count: 1; animation-duration: var(--ripple-duration); animation-timing-function: var(--bezier-ease-out), var(--bezier-ease-out); } .n-button .n-base-wave.n-base-wave--active { z-index: 1; animation-name: button-wave-spread, button-wave-opacity; } .n-button .n-button__border, .n-button .n-button__state-border { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: inherit; transition: border-color .3s var(--bezier); pointer-events: none; } .n-button .n-button__icon { margin: var(--icon-margin); margin-left: 0; height: var(--icon-size); width: var(--icon-size); max-width: var(--icon-size); font-size: var(--icon-size); position: relative; flex-shrink: 0; } .n-button .n-button__icon .n-icon-slot { height: var(--icon-size); width: var(--icon-size); position: absolute; left: 0; top: 50%; transform: translateY(-50%); display: flex; } .n-button .n-button__icon.fade-in-width-expand-transition-enter-active { overflow: hidden; transition: opacity .2s cubic-bezier(.4, 0, .2, 1) .1s, max-width .2s cubic-bezier(.4, 0, .2, 1), margin-left .2s cubic-bezier(.4, 0, .2, 1), margin-right .2s cubic-bezier(.4, 0, .2, 1); } .n-button .n-button__content { display: flex; align-items: center; flex-wrap: nowrap; } .n-button .n-button__content ~ .n-button__icon { margin: var(--icon-margin); margin-right: 0; } .n-button.n-button--block { display: flex; width: 100%; } .n-button.n-button--dashed .n-button__border, .n-button.n-button--dashed .n-button__state-border { border-style: dashed ! important; } .n-button.n-button--disabled { cursor: not-allowed; opacity: var(--opacity-disabled); } @frames button-wave-spread {from {box-shadow: 0 0 0.5px 0 var(--ripple-color); } to {box-shadow: 0 0 0.5px 4.5px var(--ripple-color); } } @keyframes button-wave-opacity { from { opacity: var(--wave-opacity); } to { opacity: 0; }}Copy the code

As you can see, button-related styles handle various scenarios using BEM naming styles:

  • Border and state-border for disabled, pressed, hover, active, etc
.n-button.n-button--color:not(.n-button--disabled):focus .n-button__state-border {

  border-color: var(--border-color-focus);

}
Copy the code
  • The pattern of ripples that appear when a button is clicked.n-button .n-base-wave
  • The style associated with icon in the button.n-button .n-button__icon
  • The style of content such as text in a button.n-button .n-button__content

You can also see that CSS Variables are reserved for various attributes in the style, including –ripple-color for box-shadow, –icon-size for icon width and height, and –bezier for transition animation. These variables leave room for various styles and themes to be customized later.

That is, when designing the style system of a component library, the component-related style templates are defined in advance using BEM style, and then the subject-related Variables that need to be customized are changed through CSS Variables to achieve the effect of customized theme.

Mount global styles

The global-related styles are mostly simple basic configurations, with the following code:

import { c } from ".. /.. /_utils/cssr"; import commonVariables from ".. /common/_common"; export default c( "body", ` margin: 0; font-size: ${commonVariables.fontSize}; font-family: ${commonVariables.fontFamily}; line-height: ${commonVariables.lineHeight}; -webkit-text-size-adjust: 100%; `, [ c( "input", ` font-family: inherit; font-size: inherit; ` ), ] );Copy the code

Margin, font-size, font-family, line-height and other related content is necessary for the compatibility of the browser CSS code standardization, the typical example is normalize.css: Make render all elements more preferably.

CommonVariables is as follows:

export default {

  fontFamily:

    'v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',

  fontFamilyMono: "v-mono, SFMono-Regular, Menlo, Consolas, Courier, monospace",



  fontWeight: "400",

  fontWeightStrong: "500",



  cubicBezierEaseInOut: "cubic-bezier(.4, 0, .2, 1)",

  cubicBezierEaseOut: "cubic-bezier(0, 0, .2, 1)",

  cubicBezierEaseIn: "cubic-bezier(.4, 0, 1, 1)",



  borderRadius: "3px",

  borderRadiusSmall: "2px",



  fontSize: "14px",

  fontSizeTiny: "12px",

  fontSizeSmall: "14px",

  fontSizeMedium: "14px",

  fontSizeLarge: "15px",

  fontSizeHuge: "16px",



  lineHeight: "1.6",



  heightTiny: "22px",

  heightSmall: "28px",

  heightMedium: "34px",

  heightLarge: "40px",

  heightHuge: "46px",



  transformDebounceScale: "scale(1)",

};
Copy the code

The above generic variables are some of the most basic “raw materials” for UI component libraries to build up, and are also the best practices in the industry that are not recommended to be modified by default. For example, there are five classes of size definition, namely tiny, Small, Medium, Large, huge, font definition, code font, etc.

Define and register CSS Variables

The main code of this section is:

const NConfigProvider = inject(configProviderInjectionKey, null); const mergedThemeRef = computed(() => { const { theme: { common: selfCommon, self, peers = {} } = {}, themeOverrides: selfOverrides = {}, builtinThemeOverrides: builtinOverrides = {}, } = props; const { common: selfCommonOverrides, peers: peersOverrides } = selfOverrides; const { common: globalCommon = undefined, [resolveId]: { common: globalSelfCommon = undefined, self: globalSelf = undefined, peers: globalPeers = {}, } = {}, } = NConfigProvider? .mergedThemeRef.value || {}; const { common: globalCommonOverrides = undefined, [resolveId]: globalSelfOverrides = {}, } = NConfigProvider? .mergedThemeOverridesRef.value || {}; const { common: globalSelfCommonOverrides, peers: globalPeersOverrides = {}, } = globalSelfOverrides; const mergedCommon = merge( {}, selfCommon || globalSelfCommon || globalCommon || defaultTheme.common, globalCommonOverrides, globalSelfCommonOverrides, selfCommonOverrides ); const mergedSelf = merge( // {}, executed every time, no need for empty obj (self || globalSelf || defaultTheme.self)? .(mergedCommon), builtinOverrides, globalSelfOverrides, selfOverrides ); return { common: mergedCommon, self: mergedSelf, peers: merge({}, defaultTheme.peers, globalPeers, peers), peerOverrides: merge({}, globalPeersOverrides, peersOverrides), };Copy the code

First through the inject from get configProviderInjectionKey related content, including configProviderInjectionKey related contents are defined in the following:

provide(configProviderInjectionKey, {

  mergedRtlRef,

  mergedIconsRef,

  mergedComponentPropsRef,

  mergedBorderedRef,

  mergedNamespaceRef,

  mergedClsPrefixRef,

  mergedLocaleRef: computed(() => {

    // xxx

  }),

  mergedDateLocaleRef: computed(() => {

    // xxx

  }),

  mergedHljsRef: computed(() => {

    // ...

  }),

  mergedThemeRef,

  mergedThemeOverridesRef

})
Copy the code

You can see RTL, icon, border, namespace, clsPrefix, locale, date, theme, themeOverrides, and almost any other configuration.

  • mergedThemeRef: adjustable theme, such as
<template> <n-config-provider :theme="darkTheme"> <app /> </n-config-provider> </template> <script> import { darkTheme }  from 'naive-ui' export default { setup() { return { darkTheme } } } </script>Copy the code
  • mergedThemeOverridesRef: Adjustable topic variables, such as
const themeOverrides = {

    common: {

      primaryColor: '#FF0000'

    },

    Button: {

      textColor: '#FF0000'

      backgroundColor: '#FFF000',

    },

    Select: {

      peers: {

        InternalSelection: {

          textColor: '#FF0000'

        }

      }

    }

    // ...

  }
Copy the code

The above two variables mainly include global common related, common related uniform variables in Button, self related variables of Button custom, and peers variables involving correlation restrictions when Button is used with other components.

After returning themeRef from the useTheme hook function, themeRef is used to assemble the various styles involved in the Button, which are handled in four main directions:

  • fontProps
  • colorProps
  • borderProps
  • sizeProps
cssVars: Computed (() => {// fontProps // colorProps // borderProps // sizeProps return {// Handles animation transition functions, transparency related variables "--bezier": cubicBezierEaseInOut, "--bezier-ease-out": cubicBezierEaseOut, "--ripple-duration": Animation-opacity rippleDuration, "-- animation-disabled ": opacityDisabled, "--wave-opacity": waveOpacity, // Handle variables related to font, color, border, size... fontProps, ... colorProps, ... borderProps, ... sizeProps, }; });Copy the code

The fontProps code is as follows:

const theme = themeRef.value;

const {

  self,

} = theme;

const {

  rippleDuration,

  opacityDisabled,

  fontWeightText,

  fontWeighGhost,

  fontWeight,

} = self;

const { dashed, type, ghost, text, color, round, circle } = props;

        // font

const fontProps = {

  fontWeight: text

    ? fontWeightText

    : ghost

    ? fontWeighGhost

    : fontWeight,

};
Copy the code

Mainly judge the font related CSS variables and values in standard state when Button is displayed as text node and transparent background.

The related codes of colorProps are as follows

let colorProps = { "--color": "initial", "--color-hover": "initial", "--color-pressed": "initial", "--color-focus": "initial", "--color-disabled": "initial", "--ripple-color": "initial", "--text-color": "initial", "--text-color-hover": "initial", "--text-color-pressed": "initial", "--text-color-focus": "initial", "--text-color-disabled": "initial", }; if (text) { const { depth } = props; const textColor = color || (type === "default" && depth ! == undefined ? self[createKey("textColorTextDepth", String(depth))] : self[createKey("textColorText", type)]); colorProps = { "--color": "#0000", "--color-hover": "#0000", "--color-pressed": "#0000", "--color-focus": "#0000", "--color-disabled": "#0000", "--ripple-color": "#0000", "--text-color": textColor, "--text-color-hover": color ? createHoverColor(color) : self[createKey("textColorTextHover", type)], "--text-color-pressed": color ? createPressedColor(color) : self[createKey("textColorTextPressed", type)], "--text-color-focus": color ? createHoverColor(color) : self[createKey("textColorTextHover", type)], "--text-color-disabled": color || self[createKey("textColorTextDisabled", type)], }; } else if (ghost || dashed) { colorProps = { "--color": "#0000", "--color-hover": "#0000", "--color-pressed": "#0000", "--color-focus": "#0000", "--color-disabled": "#0000", "--ripple-color": color || self[createKey("rippleColor", type)], "--text-color": color || self[createKey("textColorGhost", type)], "--text-color-hover": color ? createHoverColor(color) : self[createKey("textColorGhostHover", type)], "--text-color-pressed": color ? createPressedColor(color) : self[createKey("textColorGhostPressed", type)], "--text-color-focus": color ? createHoverColor(color) : self[createKey("textColorGhostHover", type)], "--text-color-disabled": color || self[createKey("textColorGhostDisabled", type)], }; } else { colorProps = { "--color": color || self[createKey("color", type)], "--color-hover": color ? createHoverColor(color) : self[createKey("colorHover", type)], "--color-pressed": color ? createPressedColor(color) : self[createKey("colorPressed", type)], "--color-focus": color ? createHoverColor(color) : self[createKey("colorFocus", type)], "--color-disabled": color || self[createKey("colorDisabled", type)], "--ripple-color": color || self[createKey("rippleColor", type)], "--text-color": color ? self.textColorPrimary : self[createKey("textColor", type)], "--text-color-hover": color ? self.textColorHoverPrimary : self[createKey("textColorHover", type)], "--text-color-pressed": color ? self.textColorPressedPrimary : self[createKey("textColorPressed", type)], "--text-color-focus": color ? self.textColorFocusPrimary : self[createKey("textColorFocus", type)], "--text-color-disabled": color ? self.textColorDisabledPrimary : self[createKey("textColorDisabled", type)], }; }Copy the code

It is mainly processed in four forms: normal, text node, ghost background transparency and dashed line mode. Related CSS attributes and values are processed for different state standards, pressed, hover, focus and disabled

The code for borderProps is as follows:

let borderProps = {

  "--border": "initial",

  "--border-hover": "initial",

  "--border-pressed": "initial",

  "--border-focus": "initial",

  "--border-disabled": "initial",

};

if (text) {

  borderProps = {

    "--border": "none",

    "--border-hover": "none",

    "--border-pressed": "none",

    "--border-focus": "none",

    "--border-disabled": "none",

  };

} else {

  borderProps = {

    "--border": self[createKey("border", type)],

    "--border-hover": self[createKey("borderHover", type)],

    "--border-pressed": self[createKey("borderPressed", type)],

    "--border-focus": self[createKey("borderFocus", type)],

    "--border-disabled": self[createKey("borderDisabled", type)],

  };

}
Copy the code

It mainly deals with the processing of five different state standards, pressed, hover, focus, disabled and so on under the display in text form and ordinary form.

The border props is defined as the border properties, and the border color properties are defined as customColorCssVars in setup:

customColorCssVars: computed(() => { const { color } = props; if (! color) return null; const hoverColor = createHoverColor(color); return { "--border-color": color, "--border-color-hover": hoverColor, "--border-color-pressed": createPressedColor(color), "--border-color-focus": hoverColor, "--border-color-disabled": color, }; })Copy the code

The code for sizeProps is as follows:

const sizeProps = { "--width": circle && ! text ? height : "initial", "--height": text ? "initial" : height, "--font-size": fontSize, "--padding": circle ? "initial" : text ? "initial" : round ? paddingRound : padding, "--icon-size": iconSize, "--icon-margin": iconMargin, "--border-radius": text ? "initial" : circle || round ? height : borderRadius, };Copy the code

Width, height, font-size, padding, icon, border, etc. Margin is handled when the global default style is mounted, which defaults to 0.

summary

Take the three steps above:

  1. Mount the button-related style class skeleton, leaving plenty of CSS Variables for custom styling
  2. Mount global default styles
  3. Assemble and define the relevant CSS Variables to populate the style class skeleton

We successfully applied CSS Render, BEM Plugin and CSS Variables to complete the overall style design of Button, which is easy to understand and customize.

The main use scenario of CSS Render is to normalize all the conditions beforehand, pre-set all the relevant CSS Variables, and then give the following

There is no shortage of necessary event handling

Naive UI provides the following types of events:

  • mousedown : handleMouseDown
  • keyuphandleKeyUp
  • keydown: handleKeyDown
  • click : handleClick
  • blur : handleBlur

Take a look at the code separately:

HandleMouseDown:

const handleMouseDown = (e) => {

  e.preventDefault();

  if (props.disabled) {

    return;

  }

  if (mergedFocusableRef.value) {

    selfRef.value?.focus({ preventScroll: true });

  }

};
Copy the code

It handles the non-response in the case of disabled, and the call to selfRef to focus and activate the corresponding style if it can focus.

HandleKeyUp:

const handleKeyUp = (e) => { switch (e.code) { case "Enter": case "NumpadEnter": if (! props.keyboard) { e.preventDefault(); return; } enterPressedRef.value = false; void nextTick(() => { if (! props.disabled) { selfRef.value? .click(); }}); }};Copy the code

The main processing Enter, NumpadEnter keys, determine whether keyboard processing support, and in the appropriate circumstances to activate button click.

HandleKeyDown:

const handleKeyDown = (e) => {

  switch (e.code) {

    case "Enter":

    case "NumpadEnter":

      if (!props.keyboard) return;

      e.preventDefault();

      enterPressedRef.value = true;

  }

};
Copy the code

It mainly processes Enter and NumpadEnter keys, checks whether keyboard processing is supported, and updates the value of enterPressedRef if appropriate, indicating that the current keyDown is over.

HandleClick:

const handleClick = (e) => { if (! props.disabled) { const { onClick } = props; if (onClick) call(onClick, e); if (! props.text) { const { value } = waveRef; if (value) { value.play(); }}}};Copy the code

The corresponding click-handling function is called based on the state and the click-ripple effect of the play button under the non-text node.

HandleBlur:

const handleBlur = () => {

  enterPressedRef.value = false;

};
Copy the code

Update the value of enterPressedRef to indicate that it is blur.

Summary and Prospect

This paper analyzes the whole process of Naive UI buttons layer by layer and source code level. It can be found that for the field of component library, most of the ideas are spent on how to Design extensible style system, from Ant Design, Element UI using Less to organize the style system. From Material Design, the style system is organized using CSS-in-JS, such as Styled components, to Naive UI, which uses CSS Render. Although there are many ways to organize style systems, But in practice, as FAR as I can see, the design style classes, corresponding styles, style extensions, and theme customization should remain broadly similar.

If you can understand the whole process of Button operation through this messy article and keep your interest in the whole source code and engineering direction of Naive UI, you can follow this logic to understand the design principles of other components, as SHOWN in the picture I showed at the beginning. The more you learn about the overall code, the easier it will feel:

Understand the source code of excellent library design, reading Daniu’s source code can help us understand the industry best practices, excellent design ideas and improve the way to write code, become more excellent developers, you and I encourage 💪!

The resources

  • css-tricks.com/bem-101/
  • www.smashingmagazine.com/2018/06/bem…
  • Getbem.com/introductio…
  • Necolas. Making. IO/normalize. C…
  • www.naiveui.com/zh-CN/os-th…
  • Github.com/07akioni/cs…
  • www.woshipm.com/ucd/4243012…
  • Getbem.com/introductio…

❤ ️ /Thank you for support /

That is all the content of this sharing. I hope it will help you

Don’t forget to share, like and bookmark your favorite things

Welcome to the public number programmer bus, from byte, shrimp, zhaoyin three brothers, share programming experience, technical dry goods and career planning, help you to avoid detours into the factory.