Skip to content
v2.5.0
English
Русский
Documentation

Slot

Slot is the low-level polymorphic composition primitive behind VegaUI asChild APIs.
slot.tsx · 5 lines
import { ButtonBase } from '@vega-ui/react';

<ButtonBase asChild variant='secondary' appearance='ghost'>
  <a href='#'>Open billing</a>
</ButtonBase>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
9 tests

Summary

Slot is the low-level polymorphic composition primitive behind VegaUI asChild APIs. It clones exactly one valid React element and merges parent props into that child.

Imports

import { Slot, type SlotProps } from '@vega-ui/react/Slot';

Exported Types

  • SlotProps<T>

Basic Usage

basic-usage.tsx · 41 lines
import type { HTMLAttributes } from 'react';
import { Slot } from '@vega-ui/react';

interface TagProps extends HTMLAttributes<HTMLElement> {
  asChild?: boolean
}

const tagStyle = {
  display: 'inline-flex',
  padding: 'var(--spacing-1) var(--spacing-5)',
  border: 'none',
  borderRadius: 999,

Variants

  • Slot itself has no visual variants.
  • Practical variation comes from the parent component that uses asChild.

Edge Cases

  • The child must be exactly one valid React element.
  • Slot changes rendering, not semantics; the chosen child element still defines the final HTML semantics.
  • Prop merging can produce surprising behavior if parent and child both define competing handlers or attributes.

Common Mistakes

  • Using Slot directly when a higher-level asChild API already exists.
  • Passing text or fragments instead of one valid element.
  • Swapping semantics unintentionally by slotting the wrong element type.

Examples

Basic

Basic: authoring an asChild component

The standard pattern: render your own element by default and Slot when asChild is set. The same Tag styling lands on a <span>, an <a>, or a <button>, and each keeps its own semantics.

basic-usage.tsx · 41 lines
import type { HTMLAttributes } from 'react';
import { Slot } from '@vega-ui/react';

interface TagProps extends HTMLAttributes<HTMLElement> {
  asChild?: boolean
}

const tagStyle = {
  display: 'inline-flex',
  padding: 'var(--spacing-1) var(--spacing-5)',
  border: 'none',
  borderRadius: 999,

Controlled/Stateful

Controlled/Stateful: chained click handlers

Slot does not replace the child's handlers: both run, the slot's first and the child's second. The log shows the order, which matters when a wrapper adds analytics or guards around an existing action.

low-level-wrapper-authoring.tsx · 34 lines
import { type ReactNode, useState } from 'react';
import { Button, Slot } from '@vega-ui/react';

interface TrackClickProps {
  event: string
  onTrack(event: string): void
  children: ReactNode
}

function TrackClick({ event, onTrack, children }: TrackClickProps) {
  return <Slot onClick={() => onTrack(`analytics: ${event}`)}>{children}</Slot>;
}

Form/Integration

Form/Integration: field wrapper that wires any control

A Field wrapper passes id, name, and aria-describedby through Slot to whatever control it wraps, so a TextArea and a native <select> get the same label and hint wiring. Slot only reaches its direct child, so wrap the control itself, not a container around it.

form-control-slot.tsx · 59 lines
import { type FormEvent, type ReactNode, useState } from 'react';
import { Button, HelperText, Label, Slot, TextArea } from '@vega-ui/react';

interface FieldProps {
  id: string
  label: string
  hint: string
  children: ReactNode
}

function Field({ id, label, hint, children }: FieldProps) {
  return (

Layout/Overlay

Layout/Overlay: custom component as a popover trigger

PopoverTrigger asChild passes popoverTarget, ARIA attributes, and a ref to the child. A custom component works only if it spreads those props and the ref onto a native <button>, as AccountChip does.

custom-overlay-trigger.tsx · 63 lines
import type { ButtonHTMLAttributes, Ref } from 'react';
import { Button, Popover, PopoverContent, PopoverTrigger, Text } from '@vega-ui/react';

interface AccountChipProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  name: string
  ref?: Ref<HTMLButtonElement>
}

function AccountChip({ name, ref, style, ...props }: AccountChipProps) {
  return (
    <button
      ref={ref}

Error

Error: plain text instead of an element

Slot needs exactly one React element to clone. Plain text throws during render; the error boundary here catches it and shows the message, and the toggle switches back to a valid <a> child.

text-child-error.tsx · 54 lines
import { Component, type ReactNode, useState } from 'react';
import { Button, ButtonBase, HelperText } from '@vega-ui/react';

interface BoundaryProps {
  children: ReactNode
}

interface BoundaryState {
  message: string
}

class SlotErrorBoundary extends Component<BoundaryProps, BoundaryState> {

Disabled

Disabled: blocked trigger that still explains itself

A natively disabled button gets no pointer or focus events, so a tooltip around it never opens. Through TooltipTrigger asChild the button uses aria-disabled instead, stays focusable, and the click handler checks the limit itself.

slotted-disabled-button.tsx · 38 lines
import { Button, Tooltip, TooltipArrow, TooltipContent, TooltipTrigger } from '@vega-ui/react';
import { useState } from 'react';

const limit = 3;

export default function SlottedDisabledButton() {
  const [used, setUsed] = useState(3);
  const blocked = used >= limit;

  const createProject = () => {
    if (blocked) return;
    setUsed(used + 1);

Edge

Edge: how props are merged

On a conflict the child wins for ordinary props such as title and for each style key. className is concatenated, id keeps the first defined value (the slot's), and both refs receive the node. The list is read from the rendered DOM.

merge-rules.tsx · 47 lines
import { useEffect, useRef, useState } from 'react';
import { Slot } from '@vega-ui/react';

export default function MergeRules() {
  const slotRef = useRef<HTMLElement>(null);
  const childRef = useRef<HTMLAnchorElement>(null);
  const [result, setResult] = useState<string[]>([]);

  useEffect(() => {
    const node = childRef.current;
    if (node === null) return;

Anatomy

Overview

Slot is a single-part low-level utility. It clones its only child and merges the passed props into that element.

Required Parts

Slot

Required. Owns the merge-and-clone behavior.

Optional Parts

There are no exported child parts.

Composition Order

  1. Slot
  2. one valid React element child

Valid Composition Patterns

basic-usage.tsx · 41 lines
import type { HTMLAttributes } from 'react';
import { Slot } from '@vega-ui/react';

interface TagProps extends HTMLAttributes<HTMLElement> {
  asChild?: boolean
}

const tagStyle = {
  display: 'inline-flex',
  padding: 'var(--spacing-1) var(--spacing-5)',
  border: 'none',
  borderRadius: 999,

Invalid Composition Patterns

More than one child

Children.only will reject the composition.

Non-element child

Plain text or invalid nodes cannot be cloned.