QRCode

Turn a link or text into a scannable, exportable QR code.

Scan with your phone to open the link

<script setup lang="ts">
  import { ref } from 'vue'
  import { FormField, Input, QRCode, Stack, Text } from '@hina-ui/vue'
  const value = ref('https://hinaui.dev')
</script>
<template>
  <Stack align="center" class="w-full max-w-sm" data-demo-qr-hero>
    <QRCode :value="value" label="Hina UI documentation" />
    <Text size="sm" tone="muted">Scan with your phone to open the link</Text>
    <FormField label="QR code content" class="w-full">
      <Input v-model="value" placeholder="https://hinaui.dev" />
    </FormField>
  </Stack>
</template>
vue

Usage

<script setup lang="ts">
  import { QRCode } from '@hina-ui/vue'
</script>

<template>
  <QRCode value="https://hinaui.dev" label="Hina UI documentation" />
</template>
vue

value is the original string to encode, including links, Unicode and plain text. The component neither visits links nor interprets their parameters. Use label to describe the purpose to assistive technology, and provide a separate accessible link when appropriate.

The complete SVG and its dimensions render during SSR. Hydration needs no measurement, canvas or QR image request. Empty strings show an empty state; data exceeding QR capacity shows an error and emits error. Correcting the value restores the code.

Examples

Size, colors and logo

Size includes the quiet zone. The square shrinks proportionally inside narrower containers. color and background accept CSS colors or tokens. The dedicated default tokens stay dark-on-light in both themes, without automatic inversion.

Use logo for a center image. Without an explicit level, codes with a logo use H; other codes use M. logoSize and logoMargin are pixels within the design size and scale with the code. The logo side is capped at a quarter of the total side. If the image fails to load, its backing is also removed, restoring the full code and emitting logo-error.

192px

<script setup lang="ts">
  import { ref } from 'vue'
  import {
    FormField,
    Inline,
    QRCode,
    SegmentedControl,
    Slider,
    Stack,
    Switch,
    Text,
    type QRCodeLevel,
  } from '@hina-ui/vue'
  const size = ref(192)
  const logo = ref(true)
  const brand = ref(false)
  const level = ref<QRCodeLevel>('H')
  const levels = ['L', 'M', 'Q', 'H'].map(value => ({ value, label: value }))
</script>
<template>
  <Stack align="center" class="w-full max-w-sm" data-demo-qr-appearance>
    <QRCode
      value="https://hinaui.dev"
      :size="size"
      :level="level"
      :logo="logo ? '/favicon.png' : undefined"
      :color="brand ? 'var(--color-brand-800)' : undefined"
    />
    <Stack class="w-full">
      <FormField label="Size">
        <Inline :wrap="false" gap="sm">
          <Slider v-model="size" :min="128" :max="256" :step="8" class="flex-1" />
          <Text size="sm" tone="muted" class="w-14 shrink-0 tabular-nums">{{ size }}px</Text>
        </Inline>
      </FormField>
      <FormField label="Error correction">
        <SegmentedControl v-model="level" :options="levels" block />
      </FormField>
      <Inline justify="between">
        <Switch v-model="logo">Center logo</Switch>
        <Switch v-model="brand">Brand color</Switch>
      </Inline>
    </Stack>
  </Stack>
</template>
vue

Keep dark modules on a light background with sufficient contrast, use a small logo, and verify with actual scanning devices. A higher correction level cannot guarantee recovery from arbitrary obstruction. The default four-module margin follows the official QR Code quiet-zone requirement; it is not pixel padding.

Status and refresh

The application controls status. The component has no expiry timer, polling or request logic. Loading, expired and scanned states hide the old code while preserving its square. The expired-state button only emits refresh; the application updates value and status after its request.

QR code expired

Refresh simulates a request here. The application controls the status.

<script setup lang="ts">
  import { onScopeDispose, ref } from 'vue'
  import { QRCode, Select, Stack, Text, type QRCodeStatus } from '@hina-ui/vue'
  const status = ref<QRCodeStatus>('expired')
  const options = [
    { value: 'active', label: 'Active' },
    { value: 'loading', label: 'Loading' },
    { value: 'expired', label: 'Expired' },
    { value: 'scanned', label: 'Scanned' },
  ]
  let timer: ReturnType<typeof setTimeout> | undefined
  function refresh() {
    clearTimeout(timer)
    status.value = 'loading'
    timer = setTimeout(() => {
      status.value = 'active'
    }, 800)
  }
  function change(value: unknown) {
    clearTimeout(timer)
    status.value = value as QRCodeStatus
  }
  onScopeDispose(() => clearTimeout(timer))
</script>
<template>
  <Stack align="center" class="w-full max-w-sm" data-demo-qr-status>
    <QRCode value="https://hinaui.dev" :status="status" @refresh="refresh" />
    <Select
      :model-value="status"
      :options="options"
      aria-label="QR code status"
      class="w-48"
      @update:model-value="change"
    />
    <Text size="sm" tone="muted" class="text-center">
      Refresh simulates a request here. The application controls the status.
    </Text>
  </Stack>
</template>
vue

Custom status

#status replaces the inactive content and receives { status, error, refresh }. Use it for application copy and actions, such as regenerating an expired share link. error represents encoding failures; handle request failures in the application.

Share link expired

Generate a fresh code to continue

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, QRCode, Stack, Text } from '@hina-ui/vue'
  const expired = ref(true)
</script>
<template>
  <Stack align="center" data-demo-qr-custom>
    <QRCode
      value="https://hinaui.dev"
      :status="expired ? 'expired' : 'active'"
      :size="224"
      @refresh="expired = false"
    >
      <template #status="{ refresh }">
        <Text weight="medium">Share link expired</Text>
        <Text size="xs" tone="muted">Generate a fresh code to continue</Text>
        <Button size="sm" variant="outline" @click="refresh">Generate again</Button>
      </template>
    </QRCode>
    <Button variant="ghost" size="sm" :disabled="expired" @click="expired = true">
      Simulate expiry
    </Button>
  </Stack>
</template>
vue

Export

Call toBlob() on the component ref to export PNG, or request type: 'image/svg+xml' for SVG. The component creates file contents; the application owns the filename and download action.

Export snapshots the current colors and embeds the logo, so the file needs no page CSS or remote logo URL. Remote logos require CORS permission. Failure to read a logo rejects the Promise rather than silently dropping it. Inactive codes cannot be exported.

<script setup lang="ts">
  import { onScopeDispose, ref } from 'vue'
  import { Download } from '@lucide/vue'
  import { Button, Inline, QRCode, Stack, Text, type QRCodeExpose } from '@hina-ui/vue'
  const code = ref<QRCodeExpose>()
  const busy = ref(false)
  const error = ref(false)
  const urls = new Set<string>()
  async function save(type: 'image/png' | 'image/svg+xml') {
    if (!code.value || busy.value) return
    busy.value = true
    error.value = false
    try {
      const blob = await code.value.toBlob({ type, scale: 3 })
      const url = URL.createObjectURL(blob)
      urls.add(url)
      const link = document.createElement('a')
      link.href = url
      link.download = type === 'image/png' ? 'hina-ui.png' : 'hina-ui.svg'
      link.click()
      setTimeout(() => {
        URL.revokeObjectURL(url)
        urls.delete(url)
      }, 1000)
    } catch {
      error.value = true
    } finally {
      busy.value = false
    }
  }
  onScopeDispose(() => urls.forEach(url => URL.revokeObjectURL(url)))
</script>
<template>
  <Stack align="center" data-demo-qr-export>
    <QRCode
      ref="code"
      value="https://hinaui.dev"
      logo="/favicon.png"
      label="Hina UI documentation"
    />
    <Inline gap="sm">
      <Button variant="outline" size="sm" :disabled="busy" @click="save('image/png')">
        <template #icon><Download /></template>
        PNG
      </Button>
      <Button variant="outline" size="sm" :disabled="busy" @click="save('image/svg+xml')">
        <template #icon><Download /></template>
        SVG
      </Button>
    </Inline>
    <Text v-if="error" tone="danger" size="sm" role="alert">Export failed. Please try again.</Text>
  </Stack>
</template>
vue
import { ref } from 'vue'
import type { QRCodeExpose } from '@hina-ui/vue'

const code = ref<QRCodeExpose>()
const png = await code.value?.toBlob({ scale: 3 })
const svg = await code.value?.toBlob({ type: 'image/svg+xml' })
ts

API

Props

Prop
Type
Default
Description
value
string
—
Required content to encode
label
string
Localized “QR code”
Accessible SVG name
size
number
192
Design side including quiet zone, in px; capped at 4096; shrinks in narrow containers
level
'L' | 'M' | 'Q' | 'H'
H with logo, otherwise M
Error correction level
margin
number
4
Quiet zone on each side, in modules; integer 0–64
color
string
--hn-qr-foreground
Module color
background
string
--hn-qr-background
Code and logo backing color
bordered
boolean
true
Outer outline
logo
string
—
Center image URL, including data URLs
logoSize
number
32
Logo side in px, capped at 25% of the design size
logoMargin
number
2
Logo backing padding in px, capped at 1/32 of the design size
status
'active' | 'loading' | 'expired' | 'scanned'
'active'
Application-controlled state
class
string
—
Root classes

Slots

SlotScope
Description
statusQRCodeStatusSlot
Inactive content: { status, error, refresh }

The resolved QRCodeState also includes empty and error, derived from encoding. The root exposes data-state and adds aria-busy during loading.

Events

Event
Payload
Description
refresh
—
Requests refresh without changing status
error
Error
Encoding failed
logo-error
Event
Center image failed to load

Expose

Name
Type
Description
element
HTMLElement | undefined
Root element
svg
SVGSVGElement | undefined
Active SVG
toBlob(options?)
Promise<Blob>
Browser export; rejects on failure

QRCodeExportOptions provides type (default 'image/png') and PNG scale (default 2, range 1–8). PNG uses size × scale, capped at 8192px per side; SVG uses size. QRCodeProps, QRCodeLevel, QRCodeStatus, QRCodeState, QRCodeStatusSlot, QRCodeExportOptions and QRCodeExpose are exported from the package root.