Skip to content

tmol4/star4

Repository files navigation

English · Русский

Warning

Since the library is in active development, many developer-friendly and organizational features are not yet present, for example:

  • Documentation website:
    It takes great time to build a docs website for a multi-framework library, you know?
  • Documentation comments:
    Unstable APIs may change frequently, so we would have to rewrite TypeDoc comments without a break.
  • Examples:
    Projects in the examples directory are actively used during development, but they do not yet serve as actual example projects.
  • Developer workflow

star4

Our official design system and UI library

Documentation · Report a bug · Request a feature

Table of contents

Features

Usage

Installation

  1. Add the Vanilla Extract plugin to the project

    The library can be used in any project which supports Vanilla Extract. All available integrations can be found at "Bundler Integration". Here's a list of currently supported integrations (at the moment of writing):

    • Astro (via @vanilla-extract/vite-plugin)
    • esbuild (via @vanilla-extract/esbuild-plugin)
    • Gatsby (via gatsby-plugin-vanilla-extract)
    • Next.js (via @vanilla-extract/next-plugin)
    • Parcel (via @vanilla-extract/parcel-transformer)
    • Remix (via @vanilla-extract/vite-plugin)
    • Rollup (via @vanilla-extract/rollup-plugin)
    • Vite (via @vanilla-extract/vite-plugin)
    • Webpack (via @vanilla-extract/webpack-plugin)
  2. Add the Vanilla Extract core library (@vanilla-extract/css):

    npm
    npm install @vanilla-extract/css
    pnpm
    pnpm add @vanilla-extract/css
    Yarn
    yarn add @vanilla-extract/css
    Bun
    bun add @vanilla-extract/css
  3. Install star4's Vanilla Extract utility package (@star4/vanilla-extract):

    npm
    npm install @star4/vanilla-extract
    pnpm
    pnpm add @star4/vanilla-extract
    Yarn
    yarn add @star4/vanilla-extract
    Bun
    bun add @star4/vanilla-extract
  4. Install star4's framework package

    • React (@star4/react):

      npm
      npm install @star4/react
      pnpm
      pnpm add @star4/react
      Yarn
      yarn add @star4/react
      Bun
      bun add @star4/react
    • Solid (@star4/solid):

      npm
      npm install @star4/solid
      pnpm
      pnpm add @star4/solid
      Yarn
      yarn add @star4/solid
      Bun
      bun add @star4/solid

Theming

In order to start using star4, you must first setup a theme. This can be done using the @star4/vanilla-extract package.

Creating a theme

A theme object can be created in an ordinary (non .css.*) file using the createTheme helper function:

import { createTheme } from "@star4/vanilla-extract";

export const { contract, theme } = createTheme({/* ... */});

This function accepts an options argument, which is an object specifying theme configuration. Some of its fields are required. Here's a minimalist example:

import { createTheme } from "@star4/vanilla-extract";

export const { contract, theme } = createTheme({
  // Use a static color scheme
  color: {},
  // Specify font families for different font styles
  typeface: {
    plain: "system-ui",
    brand: "sans-serif",
  },
  // Specify the font family used for Material Symbols icons
  component: {
    materialSymbol: {
      font: "Material Symbols Outlined",
    },
  },
});

Applying styles

The createTheme helper returns an object containing two functions: contract and theme.

First, create a theme contract using the contract helper:

// THIS EXAMPLES ASSUMES:
// - a star4 theme was created
// - `contract` was imported into this file

export const THEME = contract();

The theme function returns an object filled with CSS values, ready to be applied using (@vanilla-extract/css) inside of a .css.* file. You can use any Vanilla Extract API, depending on your needs.

Example creating a theme which follows system preference, using globalStyle and assignVars APIs:

// THIS EXAMPLES ASSUMES:
// - a star4 theme was created
// - a theme contract named `THEME` was created
// - `theme` helper and `THEME` were imported into this file

import { assignVars, globalStyle } from "@vanilla-extract/css";

const LIGHT_THEME = theme("light");
const DARK_THEME = theme("dark");

globalStyle(
  ":root, ::backdrop",
  {
    colorScheme: "light dark",
    "@media": {
      "(prefers-color-scheme: light)": {
        vars: assignVars(THEME, LIGHT_THEME),
      },
      "(prefers-color-scheme: dark)": {
        vars: assignVars(THEME, DARK_THEME),
      },
    },
  },
);

Here is a more complicated example, allowing overriding the current theme mode using a data attribute:

// THIS EXAMPLES ASSUMES:
// - a star4 theme was created
// - a theme contract named `THEME` was created
// - `theme` helper and `THEME` were imported into this file

import { assignVars, globalStyle } from "@vanilla-extract/css";

// A helper function for creating attribute selectors
const createThemeSelector = (
  themes: string | string[],
  not: boolean = false,
) => {
  const values = typeof themes === "string" ? [themes] : themes;
  const attributes = values.map(
    theme => `[data-theme="${theme}"]`,
  );
  let is = `:is(${attributes.join(",")})`;
  if(not) is = `:not(${is})`;
  return `:root${is}, ${is} ::backdrop`;
}

// Store filled theme objects for reuse
const LIGHT_THEME = theme("light");
const DARK_THEME = theme("dark");

// Possible data-theme attribute values
const DATA_THEME_LIGHT = "light";
const DATA_THEME_DARK = "dark";


// Follow system theme mode if no
// [data-theme="light"] or [data-theme="dark"]
// attribute was found
globalStyle(
  createThemeSelector([DATA_THEME_LIGHT, DATA_THEME_DARK], true),
  {
    colorScheme: "light dark",
    "@media": {
      "(prefers-color-scheme: light)": {
        vars: assignVars(THEME, LIGHT_THEME),
      },
      "(prefers-color-scheme: dark)": {
        vars: assignVars(THEME, DARK_THEME),
      },
    },
  },
);

// If <html> has a [data-theme="light"]
// attribute, apply the light theme
globalStyle(
  createThemeSelector(DATA_THEME_LIGHT),
  {
    colorScheme: "light",
    vars: assignVars(THEME, LIGHT_THEME),
  }
);

// If <html> has a [data-theme="dark"]
// attribute, apply the dark theme
globalStyle(
  createThemeSelector(DATA_THEME_DARK),
  {
    colorScheme: "dark",
    vars: assignVars(THEME, DARK_THEME),
  }
);

Color schemes

star4 uses the Material You color system, which provides two options for creating color schemes: static and dynamic. See Choosing a scheme for advice.

Static color scheme

Light



Dark

Warning

The behaviour of the color property might change in the future

The color property is responsible for color scheme configuration. Settings it to an empty object {} will assert a static scheme, while creating a dynamic scheme requires specifying a few properties.

Example of a static color scheme:

export const { contract, theme } = createTheme({
  // Empty object - using a static color scheme
  color: {},
  /* ... other fields... */
});

Example of a dynamic color scheme:

export const { contract, theme } = createTheme({
  color: {
    // A total of 9 variants are available
    variant: "tonalSpot"
    // Can be a hex, rgb CSS color, or an Hct instance
    sourceColor: "#00ff00",
    // Defaults to 0. Allows changing color scheme contrast
    contrastLevel: 0,
  },
  /* ... other fields... */
});

Fonts

Font family names containing spaces must be quoted in CSS.

Here is a helper function for merging CSS font names:

type FontFamily = (string | FontFamily)[];

const fontFamily = (...args: FontFamily): string => {
  return args
    .map(
      value => typeof value === "string"
        ? value.includes(" ")
          ? `"${value}"`
          : value
        : fontFamily(...value),
    )
    .join(", ");
}

If using variable fonts from FontSource, their font family names have Variable appended to them, e.g. Open Sans Variable, so here is a helper for FontSource fonts:

// Use system font if available,
// otherwise load the FontSource font
const fontSource = (family: string) => {
  return [`${family}`, `${family} Variable`];
};

Here's an example showcasing both helpers in action:

// Gets transformed to:
// "Roboto Flex", "Roboto Flex Variable", "Open Sans", "Open Sans Variable", Roboto, system-ui, Arial, sans-serif
const TYPEFACE_PLAIN = fontFamily(
  fontSource("Roboto Flex"),
  fontSource("Open Sans"),
  "Roboto",
  "system-ui",
  "Arial",
  "sans-serif",
);

// Gets transformed to:
// Raleway, "Raleway Variable", Manrope, "Manrope Variable", sans-serif
const TYPEFACE_BRAND = fontFamily(
  fontSource("Raleway"),
  fontSource("Manrope"),
  "sans-serif",
);
export const { contract, theme } = createTheme({
  typeface: {
    plain: TYPEFACE_PLAIN,
    brand: TYPEFACE_BRAND,
  },
  /* other fields */
});

Components

star4 framework packages export all available components, most of them contain documentation comments and code samples, so documentation for each component will not be provided here.

Identifiable components (React only)

The @star4/react package provides an additional helper with each component: .is(). This is a function which can be used to identify if a React element is a specific element. It is useful for filtering out children elements:

import { Children, type ReactNode } from "react";
import { Button } from "@star4/react";

// This component only displays buttons
// Warns when other elements are passed as children
export function OnlyButtons({ children }: { children: ReactNode }) {
  const buttons = Children.toArray(children)
    .filter(node => {
      const is = Button.is(node);
      if(!is) console.warn(
        "Invalid JSX Element passed to token resolver:",
        node,
      );
      return is;
    });

  return buttons;
}

Warning

Name and return value of the createIdentifiableElement function might change in the future.

Making your own components identifiable is also possible via the createIdentifiableElement helper. We advice you to follow the naming convention shown in the example:

import { createIdentifiableElement } from "@star4/react";

// forwardRef may also be used instead of direct assignment
const ExampleComponent = function Example() {

}

export const Example = Object.assign(
  // memo(ExampleComponent) may also be used for memoization
  ExampleComponent,
  // IS_EXAMPLE is the description of
  // a Symbol created to identify the element
  createIdentifiableElement("IS_EXAMPLE"),
);

// Use the component
<Example />

// Identify the element
Example.is(something)

Acknowledgements

star4 was only possible because of the following amazing projects:

Contributing

See CONTRIBUTING.md.

Working group

License

This project is licensed under the MIT License.