Skip to main content
You are viewing Pre-Alpha documentation.
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One

Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Atom

Low-level primitive for building polymorphic components.


Renders elementAdvanced100% coverageMar 12, 2026

Usage

This is the pattern used by every v0 component. Extend AtomProps, choose a default element, build your attributes, and let Atom handle the rendering:

vue
<script lang="ts">
  import { Atom } from '@vuetify/v0'
  import { toRef } from 'vue'
  import type { AtomProps } from '@vuetify/v0'

  export interface PaginationItemProps extends AtomProps {
    value: number
  }

  export interface PaginationItemSlotProps {
    page: number
    isSelected: boolean
    select: () => void
    attrs: Record<string, any>
  }
</script>

<script setup lang="ts">
  defineOptions({ name: 'PaginationItem' })

  defineSlots<{
    default: (props: PaginationItemSlotProps) => any
  }>()

  const {
    as = 'button',
    renderless,
    value
  } = defineProps<PaginationItemProps>()

  const slotProps = toRef((): PaginationItemSlotProps => ({
    page: value,
    isSelected: isSelected.value,
    select,
    attrs: {
      'aria-label': `Go to page ${value}`,
      'aria-current': isSelected.value ? 'page' : undefined,
      onClick: select,
    },
  }))
</script>

<template>
  <Atom
    :as
    :renderless
    v-bind="slotProps.attrs"
  >
    <slot v-bind="slotProps">
      {{ value }}
    </slot>
  </Atom>
</template>

In element mode, Atom renders the element and applies attrs to it. In renderless mode, Atom renders nothing — attrs are passed as slot props for the consumer to bind themselves.

html
<!-- Rendered mode: attrs applied automatically -->
<PaginationItem
  :value="1"
  class="data-[selected=true]:text-primary"
/>

<!-- Renderless mode: consumer applies attrs -->
<PaginationItem
  :value="1"
  renderless
  v-slot="{ page, isSelected, attrs }"
>
  <div
    v-bind="attrs"
    :class="isSelected ? 'text-primary' : ''"
  >
    Page {{ page }} {{ isSelected ? '(current)' : '' }}
  </div>
</PaginationItem>

Anatomy

vue
<script setup lang="ts">
  import { Atom } from '@vuetify/v0'
</script>

<template>
  <Atom />
</template>

Guide

Atom is an advanced, low-level primitive that enables polymorphic components. A polymorphic component can render an element with default attributes applied, or go renderless and expose its functionality entirely through slot props. Most of the time you won’t need Atom directly — it’s there for when you’re building your own components that need this flexibility.

What is a polymorphic component?

A polymorphic component is one where the consumer decides how it renders. The component provides behavior and attributes; the consumer chooses the element — or opts out of an element entirely.

Polymorphic Rendering

Use controls to zoom and pan. Click outside or press Escape to close.

Polymorphic Rendering

In rendered mode, Atom outputs the element and applies attributes automatically. In renderless mode, Atom outputs nothing — your slot content is the only DOM, and attributes are passed as slot props for you to bind yourself.

Rendered mode

Consider CheckboxRoot. It defaults to as="button" so you get native keyboard focus and activation for free:

html
<!-- CheckboxRoot renders as <button> by default -->
<Checkbox.Root v-model="agreed">
  <Checkbox.Indicator />
  I agree to the terms
</Checkbox.Root>

The consumer didn’t specify an element — CheckboxRoot chose <button> because that’s the right semantic default. But if a consumer needs a <div> instead, they override it:

html
<!-- Override: render as <div> instead -->
<Checkbox.Root v-model="agreed" as="div">
  <Checkbox.Indicator />
  I agree to the terms
</Checkbox.Root>

Renderless mode

Now consider DialogRoot. It’s renderless by default — a pure context provider that adds no DOM at all:

html
<!-- DialogRoot is renderless — no wrapper element -->
<Dialog.Root>
  <Dialog.Activator>Open</Dialog.Activator>
  <Dialog.Content>
    <Dialog.Title>Confirm</Dialog.Title>
  </Dialog.Content>
</Dialog.Root>

In renderless mode, the component provides state and behavior through its slot props. The consumer controls the DOM:

html
<!-- Group.Root is renderless by default — slot props provide selection state -->
<Selection.Root v-model="selected" v-slot="{ items }">
  <div v-for="item in items" :key="item.id">
    {{ item.value }}
  </div>
</Selection.Root>
Tip

Any v0 component can be switched between rendered and renderless. Pass renderless to strip the wrapper. Pass as="section" to add one. This works on every component because they all use Atom underneath.

Choosing a default element

When you build a component on Atom, the first decision is: should it render an element by default, or should it be renderless?

Choosing a Default

Use controls to zoom and pan. Click outside or press Escape to close.

Choosing a Default
Your component…Default toWhy
Handles clicks, focus, or keyboard inputbuttonNative keyboard activation, focus management, implicit ARIA role
Wraps a specific semantic conceptThe matching element (nav, dialog, img, ol)Browser built-ins — focus trapping, image semantics, landmark regions
Groups or lays out childrendivNo semantic meaning, just a container
Provides state/behavior onlyrenderlessNo DOM — the consumer decides how to render

Accessing the rendered element

When your component needs a reference to Atom’s rendered DOM element — for measuring, registering with a parent, or imperative DOM access — use AtomExpose to type the template ref:

vue
<script setup lang="ts">
  import { Atom } from '@vuetify/v0'
  import { useTemplateRef, watch } from 'vue'
  import type { AtomExpose } from '@vuetify/v0'

  const atomEl = useTemplateRef<AtomExpose>('atom')

  // atomEl.value?.element is the rendered HTMLElement (or null in renderless mode)
  watch(() => atomEl.value?.element, el => {
    if (!el) return
    // register with parent, measure, observe, etc.
  })
</script>

<template>
  <Atom
    ref="atom"
    :as
    :renderless
    v-bind="slotProps.attrs"
  >
    <slot v-bind="slotProps" />
  </Atom>
</template>

This is how Pagination components register their elements for width measurement, and how Splitter accesses its root for pointer event coordinates. In renderless mode, element is null — there’s no wrapper to reference.

Examples

Polymorphic Button

The same AppButton component used two ways. In rendered mode, it outputs a <button> and applies attrs automatically — you just drop it in. In renderless mode, it outputs nothing — you provide your own element and bind the attrs from the slot.

This is the core value of building on Atom: one component definition, two consumption patterns.

FileRole
AppButton.vueReusable button — extends AtomProps, builds slotProps.attrs with class and type, forwards through Atom
polymorphic.vueDemo — same component used rendered (3 variants) and renderless (consumer <a> with attrs from slot)
Variant
Size
Mode

Rendered — Atom outputs a <button> with attrs applied.

Accessibility

Atom renders semantic HTML — accessibility starts with choosing the right element.

Element Selection

The as prop determines the accessibility baseline. Using as="button" gives you keyboard focus, Enter/Space activation, and an implicit role="button" — none of which you get from as="div".

ElementYou get for free
buttonFocus, keyboard activation, implicit role
aFocus, Enter activation, link semantics
navLandmark region
dialogFocus trapping (native), Escape to close
imgAlt text support, image semantics

ARIA Passthrough

All attributes pass through to the rendered element. Set ARIA attributes directly on Atom:

html
<Atom
  as="button"
  aria-label="Close dialog"
  aria-expanded="true"
>
  ×
</Atom>

Renderless Bindings

In renderless mode, you’re responsible for applying attrs to your custom element:

html
<Atom renderless v-slot="attrs">
  <div v-bind="attrs" role="button" tabindex="0">
    Custom interactive element
  </div>
</Atom>
Warning

When using renderless mode, ensure your custom element is accessible. A <div> needs explicit role, tabindex, and keyboard event handlers to match what a native <button> provides for free.

Questions

API Reference

The following API details are for all variations of the Atom component.

Atom

Props

as

any

The HTML element to render as, or null for renderless mode

Default: "div"

renderless

boolean

When true, renders slot content directly without a wrapper element

Default: false

Slots

default

T

Default slot that receives all forwarded attributes as props

Was this page helpful?

© 2016-1970 Vuetify, LLC
Ctrl+/