---
title: "Utilities"
description: "cn, LinkHelper, resolvePartClassName, resolveSamePageHash, getReactConfig, withTagProxy, animation context and useDeferredVideoControls."
requested_language: ar
language: en
translation_notice: "This page isn't translated yet"
url: https://docs.systhema.app/ar/components/utilities
version: unreleased (main)
docs_index: https://docs.systhema.app/ar/llms.txt
---
> This page isn't translated yet. Showing English.


The helpers the components are built from, exported for your own components. Most come from both packages; four are exported by `@systhemaui/react` only.

| Export                     | `@systhemaui/react` | `@systhemaui/next`         |
| -------------------------- | ------------------- | -------------------------- |
| `cn`                       | yes                 | yes (re-export)            |
| `resolvePartClassName`     | yes                 | yes (re-export)            |
| `LinkHelper`               | yes                 | yes (own `next/link` twin) |
| `getReactConfig`           | yes                 | yes (re-export)            |
| `useDeferredVideoControls` | yes                 | yes (re-export)            |
| `resolveSamePageHash`      | yes                 | no                         |
| `withTagProxy`             | yes                 | no                         |
| `DisableAnimationsContext` | yes                 | no                         |
| `useAnimationClasses`      | yes                 | no                         |

The `@systhemaui/next` components import the last four from `@systhemaui/react`. To use them in a Next.js project, add `@systhemaui/react` to your own dependencies (pnpm does not let you import a package you don't depend on) and import them from there.

## `cn`

```ts
function cn(...classes: ClassValue[]): string
```

Joins class names with `clsx` (strings, arrays, objects of booleans) and resolves conflicting Tailwind CSS utilities with `tailwind-merge`, so the last one wins.

```tsx
import { cn } from '@systhemaui/next'

cn('px-4 py-2', isActive && 'bg-layout-alternative', 'px-6')
// 'py-2 bg-layout-alternative px-6' when isActive is true
```

## `LinkHelper`

```tsx
;<LinkHelper href="/about#team">Our team</LinkHelper>
```

The link every link-based Systhema component renders (`Link`, `Button.a`, `Card.a`, `Chip.a`, `Icon.a`, the header and footer items). In `@systhemaui/react` it is an `<a>`; in `@systhemaui/next` it is a `next/link` with all of its props. On top of a plain link it:

- calls your `onClick` first; calling `event.preventDefault()` there opts out of everything below;
- leaves new-tab, modified (`Ctrl`, `Cmd`, `Shift`, `Alt`) and non-primary clicks to the browser;
- smooth-scrolls to the target of a hash link that points at the current page (`#team`, or `/about#team` while on `/about`), resolving the target by element id, and ignores a bare `#`;
- adds `rel="noopener noreferrer"` to a `target="_blank"` link that has no `rel`.

Use it to give your own components the same link behaviour. The type is `LinkHelperProps`.

<!-- generated:props @systhemaui/react LinkHelperProps -->

| Prop                      | Type | Default | Description |
| ------------------------- | ---- | ------- | ----------- |
| …and all `<a>` attributes |      |         |             |

<!-- /generated -->

## `resolveSamePageHash`

```ts
function resolveSamePageHash(href: string, currentPathname: string): string | null
```

Exported by `@systhemaui/react` only. Returns the fragment of `href` when it points at the current page, which is how `LinkHelper` decides to smooth-scroll:

| `href`                     | On `/about` | Returns  |
| -------------------------- | ----------- | -------- |
| `'#team'`                  | any page    | `'team'` |
| `'#'`                      | any page    | `''`     |
| `'/about#team'`            | `/about`    | `'team'` |
| `'/about/?tab=1#team'`     | `/about`    | `'team'` |
| `'/contact#map'`           | `/about`    | `null`   |
| `'https://example.com/#a'` | any page    | `null`   |
| `'/about'`                 | `/about`    | `null`   |

The fragment is returned as written; decode it with `decodeURIComponent` before looking up the element. An empty string means an anchor-only href that should do nothing. Query strings and trailing slashes are ignored when the paths are compared, and any URL with a scheme or starting with `//` counts as another page.

```tsx
'use client'

import { resolveSamePageHash } from '@systhemaui/react'

export function scrollToSamePageTarget(href: string) {
  const hash = resolveSamePageHash(href, window.location.pathname)
  if (!hash) return false
  document.getElementById(decodeURIComponent(hash))?.scrollIntoView({ behavior: 'smooth' })
  return true
}
```

## `getReactConfig()`

```ts
function getReactConfig(): {
  enabled?: boolean
  animationClasses?: string
  transitionClasses?: string
  suppressHydrationWarning: true
}
```

Reads `packages.react` from `systhema.config.ts`. The components put `animationClasses` on everything that reveals on scroll and `transitionClasses` on everything that animates a state change. `suppressHydrationWarning` is always `true`: the server and the first browser render can see different class strings until `SysthemaConfigClient` hands the full config to the browser, and that mismatch is expected.

```tsx
import { getReactConfig } from '@systhemaui/next'

export function Badge({ children }: { children: React.ReactNode }) {
  const { animationClasses, suppressHydrationWarning } = getReactConfig()
  return (
    <span className={animationClasses} suppressHydrationWarning={suppressHydrationWarning}>
      {children}
    </span>
  )
}
```

## `withTagProxy`

```ts
function withTagProxy<Result>(component: (props: never) => ReactNode): Result
```

Exported by `@systhemaui/react` only. Wraps a component that takes an `as` prop so that any property names a tag: `Component.svg` renders `<Component as="svg" />`. This is how `ButtonIcon.<tag>`, `ChipIcon.<tag>` and the accordion icon work. Each tag's component is created once and cached, so `Component.span` is the same component type on every render and React never remounts its subtree. Properties that already exist on the component are returned as they are.

You type the result yourself, usually as the component plus one entry per intrinsic element:

```tsx preview title="A tag-proxied component"
import type { ComponentPropsWithoutRef, ElementType, JSX, ReactElement } from 'react'
import { withTagProxy } from '@systhemaui/react'

type BadgeProps = ComponentPropsWithoutRef<'span'> & { as?: ElementType }

function BadgeBase({ as: Component = 'span', className, ...props }: BadgeProps) {
  return <Component className={['chip', className].filter(Boolean).join(' ')} {...props} />
}

type BadgeType = typeof BadgeBase & {
  [Tag in keyof JSX.IntrinsicElements]: (props: JSX.IntrinsicElements[Tag]) => ReactElement
}

const Badge = withTagProxy<BadgeType>(BadgeBase)

export default function Demo() {
  return (
    <p>
      <Badge.strong>New</Badge.strong> <Badge.a href="#withtagproxy">Linked badge</Badge.a>
    </p>
  )
}
```

## `DisableAnimationsContext`

```ts
const DisableAnimationsContext: React.Context<boolean>
```

Exported by `@systhemaui/react` only. When an ancestor provides `true`, the components that read it (`Image`, `Video` and a nested `MediaWrapper`) leave out their scroll-animation classes. `MediaWrapper` provides it for its children, so the wrapper reveals as one unit instead of the media inside animating on top of it. Provide it in your own wrappers for the same effect:

```tsx
'use client'

import type { ReactNode } from 'react'
import { DisableAnimationsContext } from '@systhemaui/react'

export function RevealAsOne({ children }: { children: ReactNode }) {
  return (
    <div className="aos animate-fadeinup">
      <DisableAnimationsContext.Provider value={true}>{children}</DisableAnimationsContext.Provider>
    </div>
  )
}
```

The context only reaches components that read it. Others (`Heading`, `Paragraph`, `Card`, …) take `disableAnimation`, or you suppress their reveals with the `aos-disable-children` class on the wrapper.

## `useAnimationClasses`

```ts
function useAnimationClasses(disableAnimation?: boolean): string | undefined
```

Exported by `@systhemaui/react` only. Returns your configured `animationClasses`, or `undefined` when `disableAnimation` is `true` or an ancestor provides `DisableAnimationsContext` with `true`. It is a client hook, and the drop-in replacement for `!disableAnimation && getReactConfig().animationClasses` inside a `clsx` or `cn` call:

```tsx
'use client'

import type { ComponentPropsWithoutRef } from 'react'
import { cn } from '@systhemaui/next'
import { useAnimationClasses } from '@systhemaui/react'

type StatProps = ComponentPropsWithoutRef<'div'> & { disableAnimation?: boolean }

export function Stat({ className, disableAnimation, ...props }: StatProps) {
  const animationClasses = useAnimationClasses(disableAnimation)
  return <div className={cn('text-h2 color-heading', animationClasses, className)} {...props} />
}
```

## `resolvePartClassName`

```ts
function resolvePartClassName(defaultClassName: ClassValue, override?: ClassValue): string
```

Folds a consumer's class override over a part's default class with `cn`, so a conflicting utility in the override replaces the default (`'aspect-square'` drops a default `'aspect-video'`). The Posts components resolve every part this way. See [Part-resolution helpers](https://docs.systhema.app/ar/components/posts-helpers.md#part-resolution-helpers).

## `useDeferredVideoControls`

```ts
function useDeferredVideoControls(options: DeferredVideoControlsOptions): DeferredVideoControls
```

The control deferral behind `Video`'s `deferControls`, for your own `<video>` element. Pass `{ controls, deferControls, deferControlsLabel, autoPlay, onPlay, onClick, onKeyDown }`; it returns `{ controls, activationProps }`. Spread `activationProps` onto the `<video>` after your own props and use the returned `controls`. The native controls then appear on the first interaction instead of with the metadata, which keeps them from shifting the layout. See [Loading and deferred controls](https://docs.systhema.app/ar/components/video.md#loading-and-deferred-controls).

<!-- generated:props @systhemaui/react DeferredVideoControlsProps -->

| Prop         | Type                                                          | Default | Description |
| ------------ | ------------------------------------------------------------- | ------- | ----------- |
| `onPlay`     | `((event: SyntheticEvent<HTMLVideoElement, Event>) => void)`  | -       |             |
| `onClick`    | `((event: MouseEvent<HTMLVideoElement, MouseEvent>) => void)` | -       |             |
| `onKeyDown`  | `((event: KeyboardEvent<HTMLVideoElement>) => void)`          | -       |             |
| `role`       | `'button'`                                                    | -       |             |
| `tabIndex`   | `number`                                                      | -       |             |
| `aria-label` | `string`                                                      | -       |             |

<!-- /generated -->

## Related

- [Link](https://docs.systhema.app/ar/components/link.md)
- [Animation](https://docs.systhema.app/ar/styling/animation.md)
- [MediaWrapper](https://docs.systhema.app/ar/components/media-wrapper.md)
- [Configuration](https://docs.systhema.app/ar/concepts/configuration.md)
