@hdnax/genuix

A personal design system - Gene + UI/UX = Genuix.
2,631
Install
npm install @hdnax/genuix
Latest Version:0.19.1
Vue:3
Last Updated:Oct 17, 2026
Links: GitHub  ·  npm
Maintainer: huy-dna

Genuix

Status Version License

A personal design system with DNA in its name - Gene + UI/UX = Genuix (sounds like "genuine-x").

Also a place to document my findings of building a design system.

Progress tracker: Design System Home

Motivation

  • Sharpen frontend skills - both logic and visual design.
  • Learn how design systems are built and maintained.
  • Explore UI/UX best practices hands-on.
  • Build personal libraries to use across my projects.

Component Design

API Design

Some types of APIs to consider for a modular piece of UI + logic:

  • Simple reusable components.
  • Simple composables.
  • Compound components with provide/inject.
  • Vue directives.
  • Scoped slots.
  • data-* attributes.

Some may be more suitable than others depending on the scenarios.

Token Inversion Problem

When a non-primary child component sits on a solid surface, it needs inverted colors to stay readable.

One way is to have the child components aware of their rendering contexts & change their styles based on that. But it's adhoc and we can't possibly account for the possible types of surfaces they are rendered on.

Another way is to have the consumer (parent components) remap the design tokens, so the child components stay agnostic of the context.

Example: invertTokens(semantic) remaps --gui-{semantic}-* CSS custom properties on a container element:

fg       -> bg        (dark text becomes light)
fg-muted -> bg-subtle (muted text becomes subtle light)
border   -> bg-active (border visible on solid surface)
bg-hover -> solid-hover (hover state works on solid surface)

Usage: apply the returned style object on the container, and any child component reading --gui-{semantic}-* automatically gets contrast-correct values with zero overrides.

I believe Radix Themes handles it like this.

The general rule: parent owns the context, children stay generic.

This is another reason why we should use semantic design tokens.

Overridable Default Styling for Components

One problem that I faced was that:

  • I wanted to have intricate stylings for my components.
  • But, I wanted my components' stylings to be overridable.

Previously, I was styling the components like this:

<template>
  <div id="div-1" class="gui-neutral-bg-subtle" />
  <!-- Or -->
  <div id="div-2" class="my-div" />
</template>

<style scoped>
.my-div {
  ...
}
</style>

I wondered, can the consumer override these styles? If the root child component receive the fall through styles/classes they shall conflict, understandably.

So, I decided to use @layer: Component styles are placed inside @layer components. CSS layer precedence means unlayered styles always win over layered ones, regardless of specificity. This lets consumers override any component style via the class prop without !important.

This keeps the override hierarchy clean: component defaults (layered) < consumer classes (unlayered).

Semantic Styling

A problem when design GTextInput: I want to allow the text input to have multiple states (success, warning, error, default). Therefore, the consumer should be allowed to pass the current input state to the component.

In other components that I implemented, I sometimes used this pattern:

<!-- Component internals -->
<template>
  <input
    :class="{
      'text-input--error': state === 'error',
      'text-input--warning': state === 'warning',
      'text-input--success': state === 'success',
    }"
  />
</template>

<style scoped>
.text-input--error {
  @apply gui-danger-border;
}

.text-input--warning {
  @apply gui-warning-border;
}

.text-input--success {
  @apply gui-success-border;
}
</style>

However, the problem is that, what if I want the consumer to style the exact state of a component?

  1. A possible solution is to make the .text-input--error classes part of the public API, but I think this is somewhat leaky & adhoc as I have always added classes in a somewhat free-styling manner & text-input is an implementation detail.
  2. Another solution is to add props such as error-class, warning-class. However, it seems verbose and doesn't scale.
  3. Exposing via class prop leaks implementation. If an error state later adds a background change, every consumer override breaks.

So I ended up with data-* attributes, denoting the current state of the component:

<!-- Component internals -->
<template>
  <input :data-state="state" />
</template>

<style scoped>
@layer components {
  .text-input[data-state="error"] {
    @apply gui-danger-border;
  }

  .text-input[data-state="warning"] {
    @apply gui-warning-border;
  }
}
</style>
<!-- Consumer: style based on state without knowing implementation -->
<GTextInput state="error" />

The data-* attribute is also queryable by parent CSS selectors (e.g. via :has([data-state="error"])), which none of the other approaches support.

@import CSS Inside a Selector

When adding highlighting themes to GCodeBlock, I tried this approach by importing the highlight.js provided themes in the CSS selector:

<template>
  <!-- highlightTheme can be github, github-dark, etc. -->
  <div
    id="code-block"
    :theme="highlightTheme"
  >
  </div>
</template>

<style scoped>
#code-block {
  &.github {
    @import 'highlight.js/styles/github.css';
  }

  &.github-dark {
    @import 'highlight.js/styles/github-dark.css';
  }
}
</style>

This built without errors, but styles failed to be applied. I checked the browser's CSS and it yielded some malformed CSS.

I finally learnt that this was not supported, even with or without build tools.

The solution I found is using sass:

@use "sass:module";

#code-block {
  &.github {
    @include meta.load-css('highlight.js/styles/github.css');
  }
}

Should Set Explicit Button Type

By default, buttons default to the submit type, which is almost always undesriable for SPA apps.

Transitive Cascading of CSS Variables

When defining inverted color tokens, I encountered this:

:root {
  --color-a: var(--color-b);
}

.child {
  --color-b: #123456;
  color: var(--color-a);
}

I thought color would be #123456, as color is set to var(--color-a), which is originally set to var(--color-b). Therefore, I thought var(--color-a) would inherit that.

However, that wasn't the case. Maybe variables in CSS are not simply aliases...

Tailwind v4 Deprecation of Preprocessors

scss no longer works with tailwindcss v4.

Related Packages

@storybook/preset-vue-webpack

Storybook for Vue: Develop Vue Component in isolation with Hot Reloading.

1,230,873 91,393
@storybook/vue3-webpack5

Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.

915,143 91,343
@storybook/preset-vue3-webpack

Storybook for Vue 3: Develop Vue 3 Components in isolation with Hot Reloading.

914,778 91,343
@knime/kds-components

Package containing basic Vue components of the KNIME Design System

137,930
@storybook-vue/vue3

Storybook Vue 3 renderer

57,692 91,357

Version History

Version Released Vue License
0.19.1 ^3.5.42
0.19.0 ^3.5.42
0.18.1 ^3.5.42
0.18.0 ^3.5.26
0.17.0 ^3.5.26