Checkbox

Scroll Consent

Consent to a long document: the text lives in a scrollable frame and the checkbox unlocks only once it has been scrolled to the end.

  • checkbox
  • legal
  • scroll
  • consent

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/checkbox-020?lang=en

Terms of service

1. Настоящее соглашение регулирует использование сервиса и заключается между вами и оператором сервиса в момент создания учётной записи.

2. Оператор обрабатывает переданные вами данные исключительно для оказания услуги и хранит их на серверах в течение срока действия договора.

3. Вы отвечаете за сохранность пароля и за все действия, совершённые под вашей учётной записью, включая действия приглашённых вами участников.

4. Оператор вправе изменить условия, уведомив вас за тридцать дней. Продолжение использования сервиса после этого срока означает согласие с новой редакцией.

5. Соглашение может быть расторгнуто вами в любой момент из настроек учётной записи; данные удаляются в течение девяноста дней.

Долистайте текст до конца, чтобы поставить галочку.

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "checkbox-020" (Scroll Consent) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/checkbox-020.json

Registry item: https://vibeui.ru/r/checkbox-020.json
Installs to: components/vibeui/checkbox-020.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
Consent to a long document: the text lives in a scrollable frame and the checkbox unlocks only once it has been scrolled to the end.

A legal text in a scrollable frame with a consent checkbox below it. Until the document is scrolled to the end the box stays disabled and the reason is written beside it; the line changes once it is read. Zero dependencies, one file.

## 3. How to use it
import { Checkbox020 } from "@/components/vibeui/checkbox-020"

<Checkbox020
  title="Terms of service"
  paragraphs={paragraphs}
  onChange={setAccepted}
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-checkbox-020-* palette — do not swap it for your theme tokens
- tabIndex and role="region" on the text frame: without them the document cannot be scrolled by keyboard and consent is unreachable
- the explanation under the box for why it is disabled — otherwise the block looks broken
- the few-pixel slack in the end-of-scroll check: fractional heights never match exactly
- the gradient fade at the bottom edge and its disappearance once the text is read
- the unchecked default: pre-ticked consent is illegal in the EU and dishonest everywhere

## 6. You may change
- the paragraphs array — the document text, paragraph by paragraph
- title — the document heading
- label — the wording next to the checkbox
- the onChange handler and the accent through the accent prop

## 7. Rules
- Requiring a scroll is persuasion, not legal proof that anything was read.
- A very long document belongs on its own page: nobody scrolls nine screens inside a frame.
- The component stores no consent: recording the fact and the timestamp is the caller's job.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/checkbox-020.json
https://vibeui.ru/r/checkbox-020.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-checkbox-020-*. Клиентский: "use client". Обработчик onScroll сравнивает scrollTop + clientHeight со scrollHeight с запасом в восемь пикселей и разблокирует чекбокс. Рамка текста получает tabIndex и role="region" с именем, чтобы её можно было листать с клавиатуры. Градиентная тень у нижнего края показывает продолжение и гаснет по data-read на корне.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import { useRef, useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties, UIEvent } from "react"

export type Checkbox020Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children" | "onChange" | "title"
> & {
  title?: string
  paragraphs?: string[]
  label?: string
  onChange?: (accepted: boolean) => void
  accent?: string
}

// Идея компонента: длинный юридический текст в прокручиваемой рамке, а
// галочка согласия включается только после прокрутки до конца. Пока текст
// не дочитан, чекбокс выключен и рядом написано почему; внизу рамки лежит
// градиентная тень-подсказка, что текст продолжается.
const STYLES = `
:where([data-vibeui-block="checkbox-020"]){
--vibeui-checkbox-020-bg:oklch(1 0 0);
--vibeui-checkbox-020-fg:oklch(0.24 0.012 265);
--vibeui-checkbox-020-muted:oklch(0.55 0.014 265);
--vibeui-checkbox-020-border:oklch(0.9 0.006 265);
--vibeui-checkbox-020-paper:oklch(0.985 0.003 90);
--vibeui-checkbox-020-accent:oklch(0.45 0.11 260);
--vibeui-checkbox-020-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--vibeui-checkbox-020-serif:ui-serif,Georgia,"Times New Roman",serif;
}
[data-vibeui-block="checkbox-020"]{
display:block;width:100%;max-width:24rem;box-sizing:border-box;
padding:0.9375rem;border:1px solid var(--vibeui-checkbox-020-border);border-radius:0.9375rem;
background:var(--vibeui-checkbox-020-bg);
font-family:var(--vibeui-checkbox-020-font);color:var(--vibeui-checkbox-020-fg);
}
[data-vibeui-block="checkbox-020"] [data-part="title"]{
margin:0 0 0.5rem;font-size:0.9375rem;font-weight:650;letter-spacing:-0.01em;
}
[data-vibeui-block="checkbox-020"] [data-part="paper"]{position:relative}
/* Прокручиваемая рамка с tabindex: текст должен листаться и с клавиатуры,
   иначе согласие недостижимо без мыши. */
[data-vibeui-block="checkbox-020"] [data-part="scroll"]{
max-height:9rem;overflow-y:auto;overscroll-behavior:contain;
padding:0.75rem;border:1px solid var(--vibeui-checkbox-020-border);border-radius:0.625rem;
background:var(--vibeui-checkbox-020-paper);
font-family:var(--vibeui-checkbox-020-serif);font-size:0.8125rem;line-height:1.55;
}
[data-vibeui-block="checkbox-020"] [data-part="scroll"]:focus-visible{outline:2px solid var(--vibeui-checkbox-020-accent);outline-offset:2px}
[data-vibeui-block="checkbox-020"] [data-part="scroll"] p{margin:0 0 0.625rem}
[data-vibeui-block="checkbox-020"] [data-part="scroll"] p:last-child{margin-bottom:0}
/* Тень у нижнего края говорит, что текст продолжается; после дочитывания
   она убирается, и это вторая, невербальная отметка о конце. */
[data-vibeui-block="checkbox-020"] [data-part="fade"]{
position:absolute;left:1px;right:1px;bottom:1px;height:2rem;pointer-events:none;
border-radius:0 0 0.625rem 0.625rem;
background:linear-gradient(to bottom,transparent,var(--vibeui-checkbox-020-paper));
transition:opacity .2s ease;
}
[data-vibeui-block="checkbox-020"][data-read="true"] [data-part="fade"]{opacity:0}
[data-vibeui-block="checkbox-020"] label{
display:grid;grid-template-columns:auto 1fr;gap:0.625rem;align-items:start;
margin-top:0.75rem;font-size:0.8125rem;line-height:1.45;cursor:pointer;
}
[data-vibeui-block="checkbox-020"] label:has(input:disabled){cursor:not-allowed;color:var(--vibeui-checkbox-020-muted)}
[data-vibeui-block="checkbox-020"] input{
appearance:none;position:relative;flex:none;cursor:inherit;margin:0.0625rem 0 0;
width:1.0625rem;height:1.0625rem;box-sizing:border-box;
border:1.5px solid var(--vibeui-checkbox-020-border);border-radius:0.3125rem;
background:var(--vibeui-checkbox-020-bg);
transition:background-color .15s ease,border-color .15s ease;
}
[data-vibeui-block="checkbox-020"] input:disabled{background:oklch(0.95 0.004 265)}
[data-vibeui-block="checkbox-020"] input:checked{border-color:transparent;background:var(--vibeui-checkbox-020-accent)}
[data-vibeui-block="checkbox-020"] input:checked::after{
content:"";position:absolute;left:50%;top:50%;
width:0.25rem;height:0.4375rem;margin:-0.3125rem 0 0 -0.125rem;
border-right:2px solid oklch(0.99 0.01 260);border-bottom:2px solid oklch(0.99 0.01 260);
transform:rotate(45deg);
}
[data-vibeui-block="checkbox-020"] input:focus-visible{outline:2px solid var(--vibeui-checkbox-020-accent);outline-offset:2px}
[data-vibeui-block="checkbox-020"] [data-part="why"]{
margin:0.375rem 0 0;font-size:0.75rem;line-height:1.4;color:var(--vibeui-checkbox-020-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="checkbox-020"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_PARAGRAPHS = [
  "1. Настоящее соглашение регулирует использование сервиса и заключается между вами и оператором сервиса в момент создания учётной записи.",
  "2. Оператор обрабатывает переданные вами данные исключительно для оказания услуги и хранит их на серверах в течение срока действия договора.",
  "3. Вы отвечаете за сохранность пароля и за все действия, совершённые под вашей учётной записью, включая действия приглашённых вами участников.",
  "4. Оператор вправе изменить условия, уведомив вас за тридцать дней. Продолжение использования сервиса после этого срока означает согласие с новой редакцией.",
  "5. Соглашение может быть расторгнуто вами в любой момент из настроек учётной записи; данные удаляются в течение девяноста дней.",
]

/**
 * Согласие с длинным документом: галочка включается только после прокрутки
 * текста до конца. Один файл, ноль зависимостей, собственная палитра.
 */
export function Checkbox020({
  title = "Пользовательское соглашение",
  paragraphs = DEFAULT_PARAGRAPHS,
  label = "Я прочитал соглашение и согласен с его условиями",
  onChange,
  accent,
  className,
  style,
  ...props
}: Checkbox020Props) {
  const [read, setRead] = useState(false)
  const [accepted, setAccepted] = useState(false)
  const scrollRef = useRef<HTMLDivElement>(null)

  const palette = {
    ...(accent ? { "--vibeui-checkbox-020-accent": accent } : null),
    ...style,
  } as CSSProperties

  const track = (event: UIEvent<HTMLDivElement>) => {
    const node = event.currentTarget

    if (node.scrollTop + node.clientHeight >= node.scrollHeight - 8) {
      setRead(true)
    }
  }

  return (
    <>
      <style href="vibeui-checkbox-020" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="checkbox-020"
        data-read={read}
        className={className}
        style={palette}
        aria-label={title}
      >
        <h3 data-part="title">{title}</h3>
        <div data-part="paper">
          <div
            ref={scrollRef}
            data-part="scroll"
            tabIndex={0}
            role="region"
            aria-label={`Текст: ${title}`}
            onScroll={track}
          >
            {paragraphs.map((paragraph) => (
              <p key={paragraph.slice(0, 24)}>{paragraph}</p>
            ))}
          </div>
          <span data-part="fade" aria-hidden="true" />
        </div>
        <label>
          <input
            type="checkbox"
            checked={accepted}
            disabled={!read}
            onChange={(event) => {
              setAccepted(event.target.checked)
              onChange?.(event.target.checked)
            }}
          />
          <span>{label}</span>
        </label>
        <p data-part="why" role="status">
          {read
            ? "Текст прочитан — галочку можно поставить."
            : "Долистайте текст до конца, чтобы поставить галочку."}
        </p>
      </section>
    </>
  )
}