Skip to content

Permission Row

A settings row with a deny / inherit / allow tri-state toggle.

A settings row with a deny / inherit / allow tri-state toggle. Compose it from the exported registry parts so local content and behaviour stay in the application while the visual contract stays token-driven.

Root class newt-permission
import {
  Permission,
  PermissionDescription,
  PermissionInfo,
  PermissionName,
  PermissionStates,
} from "@/registry/bases/newt/ui/permission"

export default function PermissionDemo() {
  return (
    <div className="w-full max-w-md">
      <Permission>
        <PermissionInfo>
          <PermissionName>Permission name</PermissionName>
          <PermissionDescription>Permission description.</PermissionDescription>
        </PermissionInfo>
        <PermissionStates aria-label="Permission name" defaultValue="inherit" />
      </Permission>
    </div>
  )
}
<script setup lang="ts">
import { ref } from "vue"
import {
  Permission,
  PermissionDescription,
  PermissionInfo,
  PermissionName,
  PermissionStates,
  type PermissionValue,
} from "~~/registry/bases/newt/ui/permission"

const value = ref<PermissionValue>("inherit")
</script>

<template>
  <div class="w-full max-w-md">
    <Permission>
      <PermissionInfo>
        <PermissionName>Permission name</PermissionName>
        <PermissionDescription>Permission description.</PermissionDescription>
      </PermissionInfo>
      <PermissionStates v-model="value" aria-label="Permission name" />
    </Permission>
  </div>
</template>

Installation

pnpm dlx newtui@latest add permission
npx newtui@latest add permission
yarn dlx newtui@latest add permission
bunx newtui@latest add permission

Copy the source into your project

"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

type PermissionValue = "deny" | "inherit" | "allow"

const Permission = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn(
      "flex items-center justify-between border-b border-newt-border py-2.5 last:border-b-0",
      className
    )}
    {...props}
  />
))
Permission.displayName = "Permission"

const PermissionInfo = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div
    ref={ref}
    className={cn("flex flex-col gap-0.5", className)}
    {...props}
  />
))
PermissionInfo.displayName = "PermissionInfo"

const PermissionName = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn("text-sm font-medium text-newt-text-primary", className)}
    {...props}
  />
))
PermissionName.displayName = "PermissionName"

const PermissionDescription = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn("text-xs text-newt-text-muted", className)}
    {...props}
  />
))
PermissionDescription.displayName = "PermissionDescription"

const stateOptions: { value: PermissionValue; label: string; glyph: string }[] =
  [
    { value: "deny", label: "Deny", glyph: "✕" },
    { value: "inherit", label: "Inherit", glyph: "/" },
    { value: "allow", label: "Allow", glyph: "✓" },
  ]

const activeStateClasses = {
  deny: "bg-[rgba(242,63,66,0.15)] text-newt-dnd hover:text-newt-dnd",
  inherit: "bg-newt-bg-elevated text-newt-text-secondary",
  allow: "bg-[rgba(35,165,90,0.15)] text-newt-online hover:text-newt-online",
} satisfies Record<PermissionValue, string>

export interface PermissionStatesProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  "defaultValue" | "onChange"
> {
  /** Controlled value. */
  value?: PermissionValue
  /** Initial value when uncontrolled. Defaults to `"inherit"`. */
  defaultValue?: PermissionValue
  onValueChange?: (value: PermissionValue) => void
  disabled?: boolean
}

const PermissionStates = React.forwardRef<
  HTMLDivElement,
  PermissionStatesProps
>(
  (
    {
      className,
      value: valueProp,
      defaultValue = "inherit",
      onValueChange,
      disabled = false,
      ...props
    },
    ref
  ) => {
    const [internal, setInternal] =
      React.useState<PermissionValue>(defaultValue)
    const isControlled = valueProp !== undefined
    const value = isControlled ? valueProp : internal

    const select = (next: PermissionValue) => {
      if (disabled) return
      if (!isControlled) setInternal(next)
      onValueChange?.(next)
    }

    return (
      <div
        ref={ref}
        role="group"
        className={cn("flex gap-1 rounded-sm bg-newt-bg-base p-0.5", className)}
        {...props}
      >
        {stateOptions.map((option) => {
          const active = option.value === value
          return (
            <button
              key={option.value}
              type="button"
              aria-pressed={active}
              aria-label={option.label}
              disabled={disabled}
              data-state={active ? "active" : "inactive"}
              data-value={option.value}
              onClick={() => select(option.value)}
              className={cn(
                "flex h-7 w-7 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent text-sm text-newt-text-muted transition-colors duration-fast ease-newt hover:text-newt-text-secondary disabled:cursor-not-allowed disabled:opacity-50",
                active && activeStateClasses[option.value]
              )}
            >
              {option.glyph}
            </button>
          )
        })}
      </div>
    )
  }
)
PermissionStates.displayName = "PermissionStates"

export {
  Permission,
  PermissionInfo,
  PermissionName,
  PermissionDescription,
  PermissionStates,
  type PermissionValue,
}
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div
    :class="
      cn(
        'flex items-center justify-between border-b border-newt-border py-2.5 last:border-b-0',
        props.class
      )
    "
  >
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <div :class="cn('flex flex-col gap-0.5', props.class)">
    <slot />
  </div>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span :class="cn('text-sm font-medium text-newt-text-primary', props.class)">
    <slot />
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>

<template>
  <span :class="cn('text-xs text-newt-text-muted', props.class)">
    <slot />
  </span>
</template>
<script setup lang="ts">
import type { HTMLAttributes } from "vue"

import { cn } from "@/lib/utils"

import type { PermissionValue } from "."

const props = defineProps<{
  class?: HTMLAttributes["class"]
  disabled?: boolean
}>()

const model = defineModel<PermissionValue>({ default: "inherit" })

const options: { value: PermissionValue; label: string; glyph: string }[] = [
  { value: "deny", label: "Deny", glyph: "✕" },
  { value: "inherit", label: "Inherit", glyph: "/" },
  { value: "allow", label: "Allow", glyph: "✓" },
]

const activeClasses = {
  deny: "bg-[rgba(242,63,66,0.15)] text-newt-dnd hover:text-newt-dnd",
  inherit: "bg-newt-bg-elevated text-newt-text-secondary",
  allow: "bg-[rgba(35,165,90,0.15)] text-newt-online hover:text-newt-online",
} satisfies Record<PermissionValue, string>

function select(value: PermissionValue) {
  if (props.disabled) return
  model.value = value
}
</script>

<template>
  <div
    role="group"
    :class="cn('flex gap-1 rounded-sm bg-newt-bg-base p-0.5', props.class)"
  >
    <button
      v-for="option in options"
      :key="option.value"
      type="button"
      :aria-pressed="option.value === model"
      :aria-label="option.label"
      :disabled="props.disabled"
      :data-state="option.value === model ? 'active' : 'inactive'"
      :data-value="option.value"
      :class="
        cn(
          'flex h-7 w-7 cursor-pointer items-center justify-center rounded-sm border-0 bg-transparent text-sm text-newt-text-muted transition-colors duration-fast ease-newt hover:text-newt-text-secondary disabled:cursor-not-allowed disabled:opacity-50',
          option.value === model && activeClasses[option.value]
        )
      "
      @click="select(option.value)"
    >
      {{ option.glyph }}
    </button>
  </div>
</template>
export { default as Permission } from "./Permission.vue"
export { default as PermissionInfo } from "./PermissionInfo.vue"
export { default as PermissionName } from "./PermissionName.vue"
export { default as PermissionDescription } from "./PermissionDescription.vue"
export { default as PermissionStates } from "./PermissionStates.vue"

export type PermissionValue = "deny" | "inherit" | "allow"

Update the import paths

The files import @/lib/utils and sibling components through the aliases in your components.json. Point them at wherever those live in your project.

Usage

import { Permission } from "@/components/ui/permission"

export function Example() {
  return <Permission />
}
<script setup lang="ts">
import { Permission, PermissionInfo, PermissionName, PermissionDescription, PermissionStates } from "@/components/ui/permission"
</script>

<template>
  <Permission />
</template>

API reference

PermissionStates

PropType
value?PermissionValue | undefined

Controlled value.

TypePermissionValue | undefined
defaultValue?PermissionValue | undefined

Initial value when uncontrolled. Defaults to `"inherit"`.

TypePermissionValue | undefined
onValueChange?(value: PermissionValue) => void | undefined

Optional onValueChange value.

Type(value: PermissionValue) => void | undefined
disabled?boolean | undefined

Optional disabled value.

Typeboolean | undefined

The registry exports Permission, PermissionInfo, PermissionName, PermissionDescription, PermissionStates, PermissionValue. Parts without a table accept the native attributes for the element they render; React uses className, while Vue uses class and fallthrough attributes.

Accessibility

Expose deny, inherit and allow as one labelled choice group. The selected state must be announced programmatically and not communicated by colour alone.

Tokens

Permission Row reads its colours, radii and motion from the --newt-* tokens installed with the library. Override a token and every component that reads it follows, including this one — see the token layer. In the plain HTML and CSS distribution the same component is newt-permission.